Refresh token policy (Apple / Google)
logi deals with two kinds of refresh tokens:
- logi → RP — the OAuth access/refresh tokens logi issues to an RP. When you say "refresh token" in an RP integration, this is usually what you mean.
- Apple/Google → logi — the refresh tokens an upstream SSO provider issues to the logi server. An RP never sees these directly, but they affect the user experience.
This page covers the policy of the latter (upstream) and how it affects the user experience on the RP side.
Apple refresh token
Policy
- No expiry — Apple's refresh token has no explicit expiry. Once issued, it remains valid until the user disables Sign in with Apple in [iPhone Settings → Apple ID → Apps using your Apple ID], or the logi server calls
/auth/revoke. client_idbound — Apple binds the refresh token to the exactclient_idat issuance time. Calling/auth/revokewith a differentclient_idreturns a silent 4xx.- A single token in-flight — Apple's authorization code is single-use for ~5 minutes. A re-issue happens only when the user taps Sign in with Apple again.
How logi handles it
Table columns (migrations 20260504130000_add_apple_refresh_token_to_users.rb + 20260504143000_add_apple_refresh_token_client_id_to_users.rb):
| Column | Purpose |
|---|---|
users.apple_refresh_token | The plaintext refresh_token (kept internally on the server) |
users.apple_refresh_token_obtained_at | The time of a successful exchange |
users.apple_refresh_token_client_id | The client_id at issuance (must use the same value when revoking) |
Flow:
- The user performs Sign in with Apple → logi receives the identity_token + authorization_code
ExchangeAppleAuthorizationCodeJobcalls Apple/auth/tokenasynchronously → obtains the refresh_token → stores it in the columns above- When the user deletes their account,
RevokeAppleGrantJobcalls Apple/auth/revokewith the stored refresh_token
The refresh token is not for renewing access tokens
The Apple refresh token logi stores is for revocation only. The access tokens logi issues to an RP are renewed with logi's own refresh token, independent of Apple's refresh token.
Google refresh token
Policy
- 60-day inactivity expiry — Google automatically discards a refresh token unused for 60 days
- Forced re-consent after 6 months of disuse — the grant of an inactive account may expire
- Re-issue required on scope change — requesting a scope different from the existing token's scope produces a new consent screen + a new refresh token
How logi handles it
logi does not store Google refresh tokens.
Auth::GoogleSessionsController#create receives and verifies only the id_token the client (iOS/Android/Web) obtained on its own via the Google Sign-In SDK (the identity_token + raw_nonce parameters). In other words:
- The logi server never calls Google
/oauth2/v4/token - The Google refresh token exists only on the client side (or is absent entirely if the client doesn't store it)
- Columns:
users.google_sub,users.google_email,users.google_linked_at— no token column
The result: even if the Google SSO expires or the user removes logi from [Google Account → Security → Third-party apps], logi cannot detect it immediately. logi reflects the updated state only when the client presents a new id_token on the next login attempt.
Impact on the RP side
An RP's integration code deals only with logi's own access/refresh tokens. The upstream Apple/Google tokens are invisible to the RP. There is just one thing an RP needs to know:
What the RP should do
When a request with the logi access token returns 401, prompt the user to log in again. This may be logi's own token expiring, or it may be that the upstream SSO was severed (revoke / inactivity expiry / forced re-consent). The RP can handle both cases with the same UX, without needing to distinguish them.
For the rotation policy of logi's own refresh token (reuse detection → chain revoke), see Security Best Practices §Automatic token-leak neutralization.
Operational pitfalls
Google: the 60-day inactive user
The case you'll meet most often. User scenario:
- A user signs up via Google SSO and then doesn't use the app for 60+ days
- Google automatically discards the refresh token (with no particular notice to the user)
- The user relaunches the app → the Google Sign-In SDK's silent sign-in fails → the consent screen shows again
- The user feels, "Why do I have to consent again?"
Recommendation for the RP side: as long as the account-merge flow works correctly, the same canonical_sub is returned even after the new consent, so the user's data is not lost. Still, we recommend a UX copy that says, "Per our security policy, you need to consent again."
Apple: client_id branching
In logi, the same user can sign up with several Apple client_ids (e.g. com.dcodelabs.logi, com.dcodelabs.logi.mac, com.dcodelabs.logi.Clip). A separate refresh token is issued for each client_id, but logi's users.apple_refresh_token column stores only one.
Impact:
- If the user logs in with both clients, the most recent exchange result overwrites the previous one (
ExchangeAppleAuthorizationCodeJobskips ifapple_refresh_tokenalready exists — though this policy needs confirmation in the code) - On account deletion, logi calls
/auth/revokeonly for the single client stored inapple_refresh_token_client_id. The user must manually revoke the grants of the other clients in iPhone Settings
Apple: ignoring revoke 4xx
AppleRevocation is best-effort. Even if Apple /auth/revoke returns a 4xx (already expired, client_id mismatch, etc.) or 5xx, logi's account deletion still proceeds. Backup paths:
- The user manually removes it in iPhone [Settings → Apple ID → Apps using your Apple ID]
- When an Apple S2S consent-revoked notification (
Auth::AppleNotificationsController) is received by logi, logi cleans up that user
There is nothing additional for the RP to do.
Summary
| Item | Apple | |
|---|---|---|
| Does logi store the refresh token? | ✅ for revocation only | ❌ does not store |
| Expiry policy (upstream) | None (until revoked) | 60-day inactivity |
| Does logi auto-detect? | Detects via S2S notification | ❌ unknown until the next login |
| RP-side response | Prompt re-login on logi access token 401 | Same |
| User re-consent screen | On the first login after revoke | On the first login after the 60-day expiry |