Skip to content

Commit e278b40

Browse files
authored
feat: graceful refresh token rotation (#3860)
This patch adds a configuration flag which enables graceful refresh token rotation. Previously, refresh tokens could only be used once. On reuse, all tokens of that chain would be revoked. This is particularly challenging in environments, where it's difficult to make guarantees on synchronization. This could lead to refresh tokens being sent twice due to some parallel execution. To resolve this, refresh tokens can now be graceful by changing `oauth2.grant.refresh_token.grace_period=10s` (example value). During this time, a refresh token can be used multiple times to generate new refresh, ID, and access tokens. All tokens will correctly be invalidated, when the refresh token is re-used after the grace period expires, or when the delete consent endpoint is used. Closes #1831 #3770
1 parent 56fc3da commit e278b40

12 files changed

+409
-61
lines changed

driver/config/provider.go

+9
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const (
102102
KeyExcludeNotBeforeClaim = "oauth2.exclude_not_before_claim"
103103
KeyAllowedTopLevelClaims = "oauth2.allowed_top_level_claims"
104104
KeyMirrorTopLevelClaims = "oauth2.mirror_top_level_claims"
105+
KeyRefreshTokenRotationGracePeriod = "oauth2.grant.refresh_token.rotation_grace_period" // #nosec G101
105106
KeyOAuth2GrantJWTIDOptional = "oauth2.grant.jwt.jti_optional"
106107
KeyOAuth2GrantJWTIssuedDateOptional = "oauth2.grant.jwt.iat_optional"
107108
KeyOAuth2GrantJWTMaxDuration = "oauth2.grant.jwt.max_ttl"
@@ -669,3 +670,11 @@ func (p *DefaultProvider) cookieSuffix(ctx context.Context, key string) string {
669670

670671
return p.getProvider(ctx).String(key) + suffix
671672
}
673+
674+
func (p *DefaultProvider) RefreshTokenRotationGracePeriod(ctx context.Context) time.Duration {
675+
gracePeriod := p.getProvider(ctx).DurationF(KeyRefreshTokenRotationGracePeriod, 0)
676+
if gracePeriod > time.Hour {
677+
return time.Hour
678+
}
679+
return gracePeriod
680+
}

driver/config/provider_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,13 @@ func TestViperProviderValidates(t *testing.T) {
291291
assert.Equal(t, "random_salt", c.SubjectIdentifierAlgorithmSalt(ctx))
292292
assert.Equal(t, []string{"whatever"}, c.DefaultClientScope(ctx))
293293

294+
// refresh
295+
assert.Equal(t, time.Duration(0), c.RefreshTokenRotationGracePeriod(ctx))
296+
require.NoError(t, c.Set(ctx, KeyRefreshTokenRotationGracePeriod, "1s"))
297+
assert.Equal(t, time.Second, c.RefreshTokenRotationGracePeriod(ctx))
298+
require.NoError(t, c.Set(ctx, KeyRefreshTokenRotationGracePeriod, "2h"))
299+
assert.Equal(t, time.Hour, c.RefreshTokenRotationGracePeriod(ctx))
300+
294301
// urls
295302
assert.Equal(t, urlx.ParseOrPanic("https://issuer"), c.IssuerURL(ctx))
296303
assert.Equal(t, urlx.ParseOrPanic("https://public/"), c.PublicURL(ctx))

internal/config/config.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,18 @@ oauth2:
402402
session:
403403
# store encrypted data in database, default true
404404
encrypt_at_rest: true
405+
## refresh_token_rotation
406+
# By default Refresh Tokens are rotated and invalidated with each use. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.13.2 for more details
407+
refresh_token_rotation:
408+
#
409+
## grace_period
410+
#
411+
# Set the grace period for refresh tokens to be reused. Such reused tokens will result in multiple refresh tokens being issued.
412+
#
413+
# Examples:
414+
# - 5s
415+
# - 1m
416+
grace_period: 0s
405417

406418
# The secrets section configures secrets used for encryption and signing of several systems. All secrets can be rotated,
407419
# for more information on this topic navigate to:

oauth2/fosite_store_helpers.go

+102-41
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"github.com/ory/hydra/v2/oauth2/trust"
2727

28+
"github.com/ory/hydra/v2/driver/config"
2829
"github.com/ory/hydra/v2/x"
2930

3031
"github.com/ory/fosite/storage"
@@ -225,32 +226,34 @@ func TestHelperRunner(t *testing.T, store InternalRegistry, k string) {
225226
t.Run(fmt.Sprintf("case=testHelperDeleteAccessTokens/db=%s", k), testHelperDeleteAccessTokens(store))
226227
t.Run(fmt.Sprintf("case=testHelperRevokeAccessToken/db=%s", k), testHelperRevokeAccessToken(store))
227228
t.Run(fmt.Sprintf("case=testFositeJWTBearerGrantStorage/db=%s", k), testFositeJWTBearerGrantStorage(store))
229+
t.Run(fmt.Sprintf("case=testHelperRevokeRefreshTokenMaybeGracePeriod/db=%s", k), testHelperRevokeRefreshTokenMaybeGracePeriod(store))
228230
}
229231

230232
func testHelperRequestIDMultiples(m InternalRegistry, _ string) func(t *testing.T) {
231233
return func(t *testing.T) {
232-
requestId := uuid.New()
233-
mockRequestForeignKey(t, requestId, m)
234+
ctx := context.Background()
235+
requestID := uuid.New()
236+
mockRequestForeignKey(t, requestID, m)
234237
cl := &client.Client{ID: "foobar"}
235238

236239
fositeRequest := &fosite.Request{
237-
ID: requestId,
240+
ID: requestID,
238241
Client: cl,
239242
RequestedAt: time.Now().UTC().Round(time.Second),
240243
Session: NewSession("bar"),
241244
}
242245

243246
for i := 0; i < 4; i++ {
244247
signature := uuid.New()
245-
err := m.OAuth2Storage().CreateRefreshTokenSession(context.TODO(), signature, fositeRequest)
248+
err := m.OAuth2Storage().CreateRefreshTokenSession(ctx, signature, fositeRequest)
246249
assert.NoError(t, err)
247-
err = m.OAuth2Storage().CreateAccessTokenSession(context.TODO(), signature, fositeRequest)
250+
err = m.OAuth2Storage().CreateAccessTokenSession(ctx, signature, fositeRequest)
248251
assert.NoError(t, err)
249-
err = m.OAuth2Storage().CreateOpenIDConnectSession(context.TODO(), signature, fositeRequest)
252+
err = m.OAuth2Storage().CreateOpenIDConnectSession(ctx, signature, fositeRequest)
250253
assert.NoError(t, err)
251-
err = m.OAuth2Storage().CreatePKCERequestSession(context.TODO(), signature, fositeRequest)
254+
err = m.OAuth2Storage().CreatePKCERequestSession(ctx, signature, fositeRequest)
252255
assert.NoError(t, err)
253-
err = m.OAuth2Storage().CreateAuthorizeCodeSession(context.TODO(), signature, fositeRequest)
256+
err = m.OAuth2Storage().CreateAuthorizeCodeSession(ctx, signature, fositeRequest)
254257
assert.NoError(t, err)
255258
}
256259
}
@@ -475,7 +478,7 @@ func testHelperNilAccessToken(x InternalRegistry) func(t *testing.T) {
475478
m := x.OAuth2Storage()
476479
c := &client.Client{ID: "nil-request-client-id-123"}
477480
require.NoError(t, x.ClientManager().CreateClient(context.Background(), c))
478-
err := m.CreateAccessTokenSession(context.TODO(), "nil-request-id", &fosite.Request{
481+
err := m.CreateAccessTokenSession(context.Background(), "nil-request-id", &fosite.Request{
479482
ID: "",
480483
RequestedAt: time.Now().UTC().Round(time.Second),
481484
Client: c,
@@ -553,6 +556,63 @@ func testHelperRevokeAccessToken(x InternalRegistry) func(t *testing.T) {
553556
}
554557
}
555558

559+
func testHelperRevokeRefreshTokenMaybeGracePeriod(x InternalRegistry) func(t *testing.T) {
560+
return func(t *testing.T) {
561+
ctx := context.Background()
562+
563+
t.Run("Revokes refresh token when grace period not configured", func(t *testing.T) {
564+
// SETUP
565+
m := x.OAuth2Storage()
566+
567+
refreshTokenSession := fmt.Sprintf("refresh_token_%d", time.Now().Unix())
568+
err := m.CreateRefreshTokenSession(ctx, refreshTokenSession, &defaultRequest)
569+
require.NoError(t, err, "precondition failed: could not create refresh token session")
570+
571+
// ACT
572+
err = m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession)
573+
require.NoError(t, err)
574+
575+
tmpSession := new(fosite.Session)
576+
_, err = m.GetRefreshTokenSession(ctx, refreshTokenSession, *tmpSession)
577+
578+
// ASSERT
579+
// a revoked refresh token returns an error when getting the token again
580+
assert.ErrorIs(t, err, fosite.ErrInactiveToken)
581+
})
582+
583+
t.Run("refresh token enters grace period when configured,", func(t *testing.T) {
584+
// SETUP
585+
x.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, "1m")
586+
587+
// always reset back to the default
588+
t.Cleanup(func() {
589+
x.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, "0m")
590+
})
591+
592+
m := x.OAuth2Storage()
593+
594+
refreshTokenSession := fmt.Sprintf("refresh_token_%d_with_grace_period", time.Now().Unix())
595+
err := m.CreateRefreshTokenSession(ctx, refreshTokenSession, &defaultRequest)
596+
require.NoError(t, err, "precondition failed: could not create refresh token session")
597+
598+
// ACT
599+
require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession))
600+
require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession))
601+
require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession))
602+
603+
req, err := m.GetRefreshTokenSession(ctx, refreshTokenSession, nil)
604+
605+
// ASSERT
606+
// when grace period is configured the refresh token can be obtained within
607+
// the grace period without error
608+
assert.NoError(t, err)
609+
610+
assert.Equal(t, defaultRequest.GetID(), req.GetID())
611+
})
612+
}
613+
614+
}
615+
556616
func testHelperCreateGetDeletePKCERequestSession(x InternalRegistry) func(t *testing.T) {
557617
return func(t *testing.T) {
558618
m := x.OAuth2Storage()
@@ -880,6 +940,7 @@ func testFositeStoreClientAssertionJWTValid(m InternalRegistry) func(*testing.T)
880940

881941
func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
882942
return func(t *testing.T) {
943+
ctx := context.Background()
883944
grantManager := x.GrantManager()
884945
keyManager := x.KeyManager()
885946
grantStorage := x.OAuth2Storage().(rfc7523.RFC7523KeyStorage)
@@ -902,28 +963,28 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
902963
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0),
903964
}
904965

905-
storedKeySet, err := grantStorage.GetPublicKeys(context.TODO(), issuer, subject)
966+
storedKeySet, err := grantStorage.GetPublicKeys(ctx, issuer, subject)
906967
require.NoError(t, err)
907968
require.Len(t, storedKeySet.Keys, 0)
908969

909-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
970+
err = grantManager.CreateGrant(ctx, grant, publicKey)
910971
require.NoError(t, err)
911972

912-
storedKeySet, err = grantStorage.GetPublicKeys(context.TODO(), issuer, subject)
973+
storedKeySet, err = grantStorage.GetPublicKeys(ctx, issuer, subject)
913974
require.NoError(t, err)
914975
assert.Len(t, storedKeySet.Keys, 1)
915976

916-
storedKey, err := grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID)
977+
storedKey, err := grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID)
917978
require.NoError(t, err)
918979
assert.Equal(t, publicKey.KeyID, storedKey.KeyID)
919980
assert.Equal(t, publicKey.Use, storedKey.Use)
920981
assert.Equal(t, publicKey.Key, storedKey.Key)
921982

922-
storedScopes, err := grantStorage.GetPublicKeyScopes(context.TODO(), issuer, subject, publicKey.KeyID)
983+
storedScopes, err := grantStorage.GetPublicKeyScopes(ctx, issuer, subject, publicKey.KeyID)
923984
require.NoError(t, err)
924985
assert.Equal(t, grant.Scope, storedScopes)
925986

926-
storedKeySet, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID)
987+
storedKeySet, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID)
927988
require.NoError(t, err)
928989
assert.Equal(t, publicKey.KeyID, storedKeySet.Keys[0].KeyID)
929990
assert.Equal(t, publicKey.Use, storedKeySet.Keys[0].Use)
@@ -953,7 +1014,7 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
9531014

9541015
keySet2ToReturn, err := jwk.GenerateJWK(context.Background(), jose.ES256, "maria-key-2", "sig")
9551016
require.NoError(t, err)
956-
require.NoError(t, grantManager.CreateGrant(context.TODO(), trust.Grant{
1017+
require.NoError(t, grantManager.CreateGrant(ctx, trust.Grant{
9571018
ID: uuid.New(),
9581019
Issuer: issuer,
9591020
Subject: subject,
@@ -1011,22 +1072,22 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
10111072
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0),
10121073
}
10131074

1014-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
1075+
err = grantManager.CreateGrant(ctx, grant, publicKey)
10151076
require.NoError(t, err)
10161077

1017-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, grant.PublicKey.KeyID)
1078+
_, err = grantStorage.GetPublicKey(ctx, issuer, subject, grant.PublicKey.KeyID)
10181079
require.NoError(t, err)
10191080

1020-
_, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID)
1081+
_, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID)
10211082
require.NoError(t, err)
10221083

1023-
err = grantManager.DeleteGrant(context.TODO(), grant.ID)
1084+
err = grantManager.DeleteGrant(ctx, grant.ID)
10241085
require.NoError(t, err)
10251086

1026-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID)
1087+
_, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID)
10271088
assert.Error(t, err)
10281089

1029-
_, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID)
1090+
_, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID)
10301091
assert.Error(t, err)
10311092
})
10321093

@@ -1048,22 +1109,22 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
10481109
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0),
10491110
}
10501111

1051-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
1112+
err = grantManager.CreateGrant(ctx, grant, publicKey)
10521113
require.NoError(t, err)
10531114

1054-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID)
1115+
_, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID)
10551116
require.NoError(t, err)
10561117

1057-
_, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID)
1118+
_, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID)
10581119
require.NoError(t, err)
10591120

1060-
err = keyManager.DeleteKey(context.TODO(), issuer, publicKey.KeyID)
1121+
err = keyManager.DeleteKey(ctx, issuer, publicKey.KeyID)
10611122
require.NoError(t, err)
10621123

1063-
_, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID)
1124+
_, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID)
10641125
assert.Error(t, err)
10651126

1066-
_, err = grantManager.GetConcreteGrant(context.TODO(), grant.ID)
1127+
_, err = grantManager.GetConcreteGrant(ctx, grant.ID)
10671128
assert.Error(t, err)
10681129
})
10691130

@@ -1085,25 +1146,25 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
10851146
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0),
10861147
}
10871148

1088-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
1149+
err = grantManager.CreateGrant(ctx, grant, publicKey)
10891150
require.NoError(t, err)
10901151

10911152
// All three get methods should only return the public key when using the valid subject
1092-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, "any-subject-1", publicKey.KeyID)
1153+
_, err = grantStorage.GetPublicKey(ctx, issuer, "any-subject-1", publicKey.KeyID)
10931154
require.Error(t, err)
1094-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID)
1155+
_, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID)
10951156
require.NoError(t, err)
10961157

1097-
_, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, "any-subject-2", publicKey.KeyID)
1158+
_, err = grantStorage.GetPublicKeyScopes(ctx, issuer, "any-subject-2", publicKey.KeyID)
10981159
require.Error(t, err)
1099-
_, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, subject, publicKey.KeyID)
1160+
_, err = grantStorage.GetPublicKeyScopes(ctx, issuer, subject, publicKey.KeyID)
11001161
require.NoError(t, err)
11011162

1102-
jwks, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3")
1163+
jwks, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3")
11031164
require.NoError(t, err)
11041165
require.NotNil(t, jwks)
11051166
require.Empty(t, jwks.Keys)
1106-
jwks, err = grantStorage.GetPublicKeys(context.TODO(), issuer, subject)
1167+
jwks, err = grantStorage.GetPublicKeys(ctx, issuer, subject)
11071168
require.NoError(t, err)
11081169
require.NotNil(t, jwks)
11091170
require.NotEmpty(t, jwks.Keys)
@@ -1126,17 +1187,17 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
11261187
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0),
11271188
}
11281189

1129-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
1190+
err = grantManager.CreateGrant(ctx, grant, publicKey)
11301191
require.NoError(t, err)
11311192

11321193
// All three get methods should always return the public key
1133-
_, err = grantStorage.GetPublicKey(context.TODO(), issuer, "any-subject-1", publicKey.KeyID)
1194+
_, err = grantStorage.GetPublicKey(ctx, issuer, "any-subject-1", publicKey.KeyID)
11341195
require.NoError(t, err)
11351196

1136-
_, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, "any-subject-2", publicKey.KeyID)
1197+
_, err = grantStorage.GetPublicKeyScopes(ctx, issuer, "any-subject-2", publicKey.KeyID)
11371198
require.NoError(t, err)
11381199

1139-
jwks, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3")
1200+
jwks, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3")
11401201
require.NoError(t, err)
11411202
require.NotNil(t, jwks)
11421203
require.NotEmpty(t, jwks.Keys)
@@ -1159,10 +1220,10 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) {
11591220
ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(-1, 0, 0),
11601221
}
11611222

1162-
err = grantManager.CreateGrant(context.TODO(), grant, publicKey)
1223+
err = grantManager.CreateGrant(ctx, grant, publicKey)
11631224
require.NoError(t, err)
11641225

1165-
keys, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3")
1226+
keys, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3")
11661227
require.NoError(t, err)
11671228
assert.Len(t, keys.Keys, 0)
11681229
})

0 commit comments

Comments
 (0)