OAuth callbacks (POST /auth/google/callback, POST /auth/microsoft/callback)
issued refresh tokens via create_refresh_token() but never persisted the JTI
in the refresh_tokens table. The /auth/refresh rotation logic does a
conditional UPDATE that requires a matching unrevoked row; without it the
first refresh attempt 401s with "Refresh token has been revoked" and OAuth
users get effectively logged out after the ~5 minute access-token expiry.
- Promote _store_refresh_token to module-public store_refresh_token in
app.api.endpoints.auth (existing callers in /login, /login/json, /refresh
updated in-place — same module, just renamed).
- OAuth callbacks now call store_refresh_token(...) + db.commit() after
_sign_in_or_register returns. _sign_in_or_register already commits the
user/account/identity rows; the refresh-token row gets its own commit.
- Tests:
- test_oauth_google_callback_stores_refresh_token_jti — asserts the JTI
hash is in refresh_tokens after a Google callback.
- test_oauth_refresh_works_after_oauth_signup — full e2e: callback -> use
returned refresh token at /auth/refresh -> 200 with rotated tokens.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>