fix: #3630881 Store the client on refresh tokens
Closes https://www.drupal.org/project/simple_oauth/issues/3630881
What changed
RefreshTokenEntityNormalizer::normalize() now sets client from the client of the access token the refresh token belongs to. Before, it saved only value and expire, so every refresh_token row had an empty client field and ExpiredCollector::collectForClient($client, TRUE) never returned refresh tokens.
Existing refresh tokens keep an empty client. The row has no link to its access token, so there's nothing to backfill from. They expire on their own.
Behavior
collectForClient()without$include_refresh,collectForAccount(), and consumer and user update invalidation all filter out therefresh_tokenbundle. Their behavior doesn't change.collectForClient($client, TRUE)now returns the client's refresh tokens. With #3630880 (!240 (merged)), deleting a consumer also deletes its refresh tokens.- Related:
simple_oauth_server_metadata'sTokenRevocationService::validateOwnership()returnsFALSEwhen a token has no client. I haven't verified whether its lookup finds refresh tokens, but this removes one reason RFC 7009 revocation of a refresh token would fail.
Testing
RefreshTokenTest::testRefreshTokenReferencesClient() asserts that the refresh token issued in the authorization code flow references the consumer and that collectForClient($client, TRUE) returns it. Without the fix, it fails with null for the client.
AI-Generated: Yes. I wrote this fix and description with Claude Code and reviewed both.