feat: add in-memory token caching - #114
Draft
andrewheard wants to merge 3 commits into
Draft
Conversation
When GACAppCheck refreshes a token, write failures to Keychain storage (e.g., in swift test or un-entitled environments) rejected the promise, discarding the fetched token and returning GACAppCheckErrorCodeKeychain. Treat storage write errors as non-fatal by logging a warning with code GACLoggerAppCheckMessageCodeTokenStorageFailed and returning the valid token. Maintain an in-memory token cache to serve unexpired tokens without repeated Keychain IPC, falling back to memory when Keychain is unavailable. Invalidate the in-memory cache on forced refresh, and expand unit test coverage.
Add the missing `.loggerAppCheckMessageCodeTokenStorageFailed` case to the `AppCheckCoreMessageCode` switch statement in `AppCheckAPITests` to resolve an exhaustive switch warning on CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Added an in-memory token cache and made Keychain token caching failures non-fatal in
GACAppCheck.Previously,
GACAppCheck.refreshTokenfailed the entire token request withGACAppCheckErrorCodeKeychainif writing to Keychain storage failed, discarding the valid fetched token. This meant that workflows in environments without Keychain entitlements, such asswift teston macOS and CI, were unsupported. Now, tokens are added to an in-memory cache that functions even if writes to Keychain can't be performed and offers faster read times in environments where the Keychain is available.refreshToken, logged a warning (GACLoggerAppCheckMessageCodeTokenStorageFailed), and returned the valid token. Keychain writes are still attempted on every refresh to preserve persistence across cold launches.inMemoryTokentoGACAppCheck. Unexpired tokens are now served directly from memory, eliminating redundant Keychain IPC queries.inMemoryTokenwhenforcingRefresh: YESwas requested so revoked tokens are not retained if a refresh fails.GACAppCheckTests.mto cover Keychain write error resilience, in-memory cache hits, forced refresh invalidation, and expiration fallback.Walkthrough - In-Memory Fallback & Non-Fatal Keychain Storage
We investigated the token caching and Keychain error handling behavior in
AppCheckCoreand resolved the issue where Keychain write failures (such asthose encountered in
swift testor un-entitled environments) discardedvalid tokens and failed operations with
GACAppCheckErrorCodeKeychain.Summary of Changes
GACAppCheckErrors.h
GACAppCheck.m
@property(nonatomic, strong, nullable) GACAppCheckToken *inMemoryToken;.@synchronized(self), relying on queue confinementconsistent with
ongoingRetrieveOrRefreshTokenPromiseonFBLPromise.defaultDispatchQueue.getCachedValidTokenForcingRefresh:explicitly setsself.inMemoryToken = nil;when
forcingRefresh: YESis passed. If a token was revoked and a refreshfails, the revoked token is no longer retained in memory.
inMemoryTokenfirst via extracted helper- (BOOL)isTokenExpiredOrExpiresSoon:(GACAppCheckToken *)token.kTokenExpirationThreshold(5 min), checksKeychain storage.
provider.
refreshToken,[self.storage setToken:token]catches Keychain writefailures with
.recover(^id _Nullable(NSError *_Nonnull error) { ... }).2004and returns the validtoken, allowing the caller to receive the fetched token without failing.preserving persistence across app cold launches in typical environments.
securityd) when a valid token isalready held in memory significantly reduces token retrieval overhead.
GACAppCheckTests.m
testGetToken_WhenCalledSeveralTimesError_ThenThereIsOnlyOneOperationto test operation merging on provider failures now that storage failures
are non-fatal.
testGetToken_WhenCalledSeveralTimesSuccess_ThenThereIsOnlyOneOperationto verify subsequent calls reuse the in-memory cached token.
testGetToken_WhenStorageFails_ThenTokenReturnedAndCachedInMemorytoverify that Keychain write errors log a warning, succeed with the valid
token, and cache it in memory.
testGetToken_WhenForcingRefresh_ThenInMemoryCacheIsBypassedtoverify the refreshed token is served on subsequent calls.
testGetToken_WhenForcingRefresh_ThenInMemoryTokenIsInvalidatedtoverify that
forcingRefresh: YESinvalidates the in-memory token even ifthe refresh fails.
testGetToken_WhenInMemoryTokenExpires_ThenRefreshesWithProvidertoverify that an expiring in-memory token triggers a refresh.
Verification Results
Automated Tests
Ran the full test suite using
swift testand formatted withxcsift:Output:
All 136 unit tests passed with zero errors and zero warnings.