/* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTimer.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" NS_ASSUME_NONNULL_BEGIN static const NSTimeInterval kInitialBackoffTimeInterval = 30; static const NSTimeInterval kMaximumBackoffTimeInterval = 16 * 60; static const NSTimeInterval kMinimumAutoRefreshTimeInterval = 60; // 1 min. /// How much time in advance to auto-refresh token before it's expiration. E.g. 0.5 means that the /// token will be refreshed half way through it's intended time to live. static const double kAutoRefreshFraction = 0.5; @interface FIRAppCheckTokenRefresher () @property(nonatomic, readonly) dispatch_queue_t refreshQueue; @property(nonatomic, readonly) id settings; @property(nonatomic, readonly) FIRTimerProvider timerProvider; @property(atomic, nullable) id timer; @property(atomic) NSUInteger retryCount; /// Initial refresh result to be used when `tokenRefreshHandler` has been sent. @property(nonatomic, nullable) FIRAppCheckTokenRefreshResult *initialRefreshResult; @end @implementation FIRAppCheckTokenRefresher @synthesize tokenRefreshHandler = _tokenRefreshHandler; - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult timerProvider:(FIRTimerProvider)timerProvider settings:(id)settings { self = [super init]; if (self) { _refreshQueue = dispatch_queue_create("com.firebase.FIRAppCheckTokenRefresher", DISPATCH_QUEUE_SERIAL); _initialRefreshResult = refreshResult; _timerProvider = timerProvider; _settings = settings; } return self; } - (instancetype)initWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult settings:(id)settings { return [self initWithRefreshResult:refreshResult timerProvider:[FIRAppCheckTimer timerProvider] settings:settings]; } - (void)dealloc { [self cancelTimer]; } - (void)setTokenRefreshHandler:(FIRAppCheckTokenRefreshBlock)tokenRefreshHandler { @synchronized(self) { _tokenRefreshHandler = tokenRefreshHandler; // Check if handler is being set for the first time and if yes then schedule first refresh. if (tokenRefreshHandler && self.initialRefreshResult) { FIRAppCheckTokenRefreshResult *initialTokenRefreshResult = self.initialRefreshResult; self.initialRefreshResult = nil; [self scheduleWithTokenRefreshResult:initialTokenRefreshResult]; } } } - (FIRAppCheckTokenRefreshBlock)tokenRefreshHandler { @synchronized(self) { return _tokenRefreshHandler; } } - (void)updateWithRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { switch (refreshResult.status) { case FIRAppCheckTokenRefreshStatusNever: case FIRAppCheckTokenRefreshStatusSuccess: self.retryCount = 0; break; case FIRAppCheckTokenRefreshStatusFailure: self.retryCount += 1; break; } [self scheduleWithTokenRefreshResult:refreshResult]; } - (void)refresh { if (self.tokenRefreshHandler == nil) { return; } if (!self.settings.isTokenAutoRefreshEnabled) { return; } __auto_type __weak weakSelf = self; self.tokenRefreshHandler(^(FIRAppCheckTokenRefreshResult *refreshResult) { __auto_type strongSelf = weakSelf; [strongSelf updateWithRefreshResult:refreshResult]; }); } - (void)scheduleWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { // Schedule the refresh only when allowed. if (self.settings.isTokenAutoRefreshEnabled) { NSDate *refreshDate = [self nextRefreshDateWithTokenRefreshResult:refreshResult]; [self scheduleRefreshAtDate:refreshDate]; } } - (void)scheduleRefreshAtDate:(NSDate *)refreshDate { [self cancelTimer]; NSTimeInterval scheduleInSec = [refreshDate timeIntervalSinceNow]; __auto_type __weak weakSelf = self; dispatch_block_t refreshHandler = ^{ __auto_type strongSelf = weakSelf; [strongSelf refresh]; }; // Refresh straight away if the refresh time is too close. if (scheduleInSec <= 0) { dispatch_async(self.refreshQueue, refreshHandler); return; } self.timer = self.timerProvider(refreshDate, self.refreshQueue, refreshHandler); } - (void)cancelTimer { [self.timer invalidate]; } - (NSDate *)nextRefreshDateWithTokenRefreshResult:(FIRAppCheckTokenRefreshResult *)refreshResult { switch (refreshResult.status) { case FIRAppCheckTokenRefreshStatusSuccess: { NSTimeInterval timeToLive = [refreshResult.tokenExpirationDate timeIntervalSinceDate:refreshResult.tokenReceivedAtDate]; timeToLive = MAX(timeToLive, 0); // Refresh in 50% of TTL + 5 min. NSTimeInterval targetRefreshSinceReceivedDate = timeToLive * kAutoRefreshFraction + 5 * 60; NSDate *targetRefreshDate = [refreshResult.tokenReceivedAtDate dateByAddingTimeInterval:targetRefreshSinceReceivedDate]; // Don't schedule later than expiration date. NSDate *refreshDate = [targetRefreshDate earlierDate:refreshResult.tokenExpirationDate]; // Don't schedule a refresh earlier than in 1 min from now. if ([refreshDate timeIntervalSinceNow] < kMinimumAutoRefreshTimeInterval) { refreshDate = [NSDate dateWithTimeIntervalSinceNow:kMinimumAutoRefreshTimeInterval]; } return refreshDate; } break; case FIRAppCheckTokenRefreshStatusFailure: { // Repeat refresh attempt later. NSTimeInterval backoffTime = [[self class] backoffTimeForRetryCount:self.retryCount]; return [NSDate dateWithTimeIntervalSinceNow:backoffTime]; } break; case FIRAppCheckTokenRefreshStatusNever: // Refresh ASAP. return [NSDate date]; break; } } #pragma mark - Backoff + (NSTimeInterval)backoffTimeForRetryCount:(NSInteger)retryCount { if (retryCount == 0) { // No backoff for the first attempt. return 0; } NSTimeInterval exponentialInterval = kInitialBackoffTimeInterval * pow(2, retryCount - 1) + [self randomMilliseconds]; return MIN(exponentialInterval, kMaximumBackoffTimeInterval); } + (NSTimeInterval)randomMilliseconds { int32_t random_millis = ABS(arc4random() % 1000); return (double)random_millis * 0.001; } @end NS_ASSUME_NONNULL_END