GACAppCheck.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /*
  2. * Copyright 2020 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "AppCheck/Sources/Public/AppCheck/GACAppCheck.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import <AppCheckInterop/AppCheckInterop.h>
  23. #import "AppCheck/Sources/Public/AppCheck/GACAppCheckErrors.h"
  24. #import "AppCheck/Sources/Public/AppCheck/GACAppCheckProvider.h"
  25. #import "AppCheck/Sources/Public/AppCheck/GACAppCheckSettings.h"
  26. #import "AppCheck/Sources/Public/AppCheck/GACAppCheckToken.h"
  27. #import "AppCheck/Sources/Public/AppCheck/GACAppCheckTokenDelegate.h"
  28. #import "AppCheck/Sources/Core/Errors/GACAppCheckErrorUtil.h"
  29. #import "AppCheck/Sources/Core/GACAppCheckLogger.h"
  30. #import "AppCheck/Sources/Core/GACAppCheckTokenResult.h"
  31. #import "AppCheck/Sources/Core/Storage/GACAppCheckStorage.h"
  32. #import "AppCheck/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h"
  33. #import "AppCheck/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h"
  34. NS_ASSUME_NONNULL_BEGIN
  35. // TODO(andrewheard): Remove from generic App Check SDK.
  36. // FIREBASE_APP_CHECK_ONLY_BEGIN
  37. static NSString *const kGACAppCheckTokenAutoRefreshEnabledUserDefaultsPrefix =
  38. @"GACAppCheckTokenAutoRefreshEnabled_";
  39. static NSString *const kGACAppCheckTokenAutoRefreshEnabledInfoPlistKey =
  40. @"FirebaseAppCheckTokenAutoRefreshEnabled";
  41. // FIREBASE_APP_CHECK_ONLY_END
  42. static const NSTimeInterval kTokenExpirationThreshold = 5 * 60; // 5 min.
  43. static NSString *const kDummyFACTokenValue = @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==";
  44. @interface GACAppCheck ()
  45. @property(nonatomic, readonly) NSString *instanceName;
  46. @property(nonatomic, readonly) id<GACAppCheckProvider> appCheckProvider;
  47. @property(nonatomic, readonly) id<GACAppCheckStorageProtocol> storage;
  48. @property(nonatomic, readonly) id<GACAppCheckSettingsProtocol> settings;
  49. @property(nonatomic, readonly, nullable) id<GACAppCheckTokenRefresherProtocol> tokenRefresher;
  50. @property(nonatomic, nullable) FBLPromise<GACAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;
  51. @end
  52. @implementation GACAppCheck
  53. #pragma mark - Internal
  54. - (instancetype)initWithInstanceName:(NSString *)instanceName
  55. appCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
  56. storage:(id<GACAppCheckStorageProtocol>)storage
  57. tokenRefresher:(id<GACAppCheckTokenRefresherProtocol>)tokenRefresher
  58. settings:(id<GACAppCheckSettingsProtocol>)settings {
  59. self = [super init];
  60. if (self) {
  61. _instanceName = instanceName;
  62. _appCheckProvider = appCheckProvider;
  63. _storage = storage;
  64. _tokenRefresher = tokenRefresher;
  65. _settings = settings;
  66. __auto_type __weak weakSelf = self;
  67. tokenRefresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _Nonnull completion) {
  68. __auto_type strongSelf = weakSelf;
  69. [strongSelf periodicTokenRefreshWithCompletion:completion];
  70. };
  71. }
  72. return self;
  73. }
  74. #pragma mark - Public
  75. - (instancetype)initWithInstanceName:(NSString *)instanceName
  76. appCheckProvider:(id<GACAppCheckProvider>)appCheckProvider
  77. settings:(id<GACAppCheckSettingsProtocol>)settings
  78. resourceName:(NSString *)resourceName
  79. keychainAccessGroup:(nullable NSString *)accessGroup {
  80. GACAppCheckTokenRefreshResult *refreshResult =
  81. [[GACAppCheckTokenRefreshResult alloc] initWithStatusNever];
  82. GACAppCheckTokenRefresher *tokenRefresher =
  83. [[GACAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult settings:settings];
  84. NSString *tokenKey =
  85. [NSString stringWithFormat:@"app_check_token.%@.%@", instanceName, resourceName];
  86. GACAppCheckStorage *storage = [[GACAppCheckStorage alloc] initWithTokenKey:tokenKey
  87. accessGroup:accessGroup];
  88. return [self initWithInstanceName:instanceName
  89. appCheckProvider:appCheckProvider
  90. storage:storage
  91. tokenRefresher:tokenRefresher
  92. settings:settings];
  93. }
  94. - (void)tokenForcingRefresh:(BOOL)forcingRefresh
  95. completion:(void (^)(GACAppCheckToken *_Nullable token,
  96. NSError *_Nullable error))handler {
  97. [self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
  98. .then(^id _Nullable(GACAppCheckToken *token) {
  99. handler(token, nil);
  100. return token;
  101. })
  102. .catch(^(NSError *_Nonnull error) {
  103. handler(nil, [GACAppCheckErrorUtil publicDomainErrorWithError:error]);
  104. });
  105. }
  106. - (void)limitedUseTokenWithCompletion:(void (^)(GACAppCheckToken *_Nullable token,
  107. NSError *_Nullable error))handler {
  108. [self limitedUseToken]
  109. .then(^id _Nullable(GACAppCheckToken *token) {
  110. handler(token, nil);
  111. return token;
  112. })
  113. .catch(^(NSError *_Nonnull error) {
  114. handler(nil, [GACAppCheckErrorUtil publicDomainErrorWithError:error]);
  115. });
  116. }
  117. #pragma mark - GACAppCheckInterop
  118. - (void)getTokenForcingRefresh:(BOOL)forcingRefresh
  119. completion:(GACAppCheckTokenHandlerInterop)handler {
  120. [self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
  121. .then(^id _Nullable(GACAppCheckToken *token) {
  122. GACAppCheckTokenResult *result = [[GACAppCheckTokenResult alloc] initWithToken:token.token
  123. error:nil];
  124. handler(result);
  125. return result;
  126. })
  127. .catch(^(NSError *_Nonnull error) {
  128. GACAppCheckTokenResult *result =
  129. [[GACAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
  130. handler(result);
  131. });
  132. }
  133. - (void)getLimitedUseTokenWithCompletion:(GACAppCheckTokenHandlerInterop)handler {
  134. [self limitedUseToken]
  135. .then(^id _Nullable(GACAppCheckToken *token) {
  136. GACAppCheckTokenResult *result = [[GACAppCheckTokenResult alloc] initWithToken:token.token
  137. error:nil];
  138. handler(result);
  139. return result;
  140. })
  141. .catch(^(NSError *_Nonnull error) {
  142. GACAppCheckTokenResult *result =
  143. [[GACAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
  144. handler(result);
  145. });
  146. }
  147. #pragma mark - FAA token cache
  148. - (FBLPromise<GACAppCheckToken *> *)retrieveOrRefreshTokenForcingRefresh:(BOOL)forcingRefresh {
  149. return [FBLPromise do:^id _Nullable {
  150. if (self.ongoingRetrieveOrRefreshTokenPromise == nil) {
  151. // Kick off a new operation only when there is not an ongoing one.
  152. self.ongoingRetrieveOrRefreshTokenPromise =
  153. [self createRetrieveOrRefreshTokenPromiseForcingRefresh:forcingRefresh]
  154. // Release the ongoing operation promise on completion.
  155. .then(^GACAppCheckToken *(GACAppCheckToken *token) {
  156. self.ongoingRetrieveOrRefreshTokenPromise = nil;
  157. return token;
  158. })
  159. .recover(^NSError *(NSError *error) {
  160. self.ongoingRetrieveOrRefreshTokenPromise = nil;
  161. return error;
  162. });
  163. }
  164. return self.ongoingRetrieveOrRefreshTokenPromise;
  165. }];
  166. }
  167. - (FBLPromise<GACAppCheckToken *> *)createRetrieveOrRefreshTokenPromiseForcingRefresh:
  168. (BOOL)forcingRefresh {
  169. return [self getCachedValidTokenForcingRefresh:forcingRefresh].recover(
  170. ^id _Nullable(NSError *_Nonnull error) {
  171. return [self refreshToken];
  172. });
  173. }
  174. - (FBLPromise<GACAppCheckToken *> *)getCachedValidTokenForcingRefresh:(BOOL)forcingRefresh {
  175. if (forcingRefresh) {
  176. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  177. [rejectedPromise reject:[GACAppCheckErrorUtil cachedTokenNotFound]];
  178. return rejectedPromise;
  179. }
  180. return [self.storage getToken].then(^id(GACAppCheckToken *_Nullable token) {
  181. if (token == nil) {
  182. return [GACAppCheckErrorUtil cachedTokenNotFound];
  183. }
  184. BOOL isTokenExpiredOrExpiresSoon =
  185. [token.expirationDate timeIntervalSinceNow] < kTokenExpirationThreshold;
  186. if (isTokenExpiredOrExpiresSoon) {
  187. return [GACAppCheckErrorUtil cachedTokenExpired];
  188. }
  189. return token;
  190. });
  191. }
  192. - (FBLPromise<GACAppCheckToken *> *)refreshToken {
  193. return [FBLPromise
  194. wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
  195. [self.appCheckProvider getTokenWithCompletion:handler];
  196. }]
  197. .then(^id _Nullable(GACAppCheckToken *_Nullable token) {
  198. return [self.storage setToken:token];
  199. })
  200. .then(^id _Nullable(GACAppCheckToken *_Nullable token) {
  201. // TODO: Make sure the self.tokenRefresher is updated only once. Currently the timer will be
  202. // updated twice in the case when the refresh triggered by self.tokenRefresher, but it
  203. // should be fine for now as it is a relatively cheap operation.
  204. __auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc]
  205. initWithStatusSuccessAndExpirationDate:token.expirationDate
  206. receivedAtDate:token.receivedAtDate];
  207. [self.tokenRefresher updateWithRefreshResult:refreshResult];
  208. if (self.tokenDelegate) {
  209. NSString *tokenValue = token ? token.token : nil;
  210. [self.tokenDelegate didUpdateWithToken:tokenValue];
  211. }
  212. return token;
  213. });
  214. }
  215. - (FBLPromise<GACAppCheckToken *> *)limitedUseToken {
  216. return
  217. [FBLPromise wrapObjectOrErrorCompletion:^(
  218. FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
  219. [self.appCheckProvider getTokenWithCompletion:handler];
  220. }].then(^id _Nullable(GACAppCheckToken *_Nullable token) {
  221. return token;
  222. });
  223. }
  224. #pragma mark - Token auto refresh
  225. - (void)periodicTokenRefreshWithCompletion:(GACAppCheckTokenRefreshCompletion)completion {
  226. [self retrieveOrRefreshTokenForcingRefresh:NO]
  227. .then(^id _Nullable(GACAppCheckToken *_Nullable token) {
  228. __auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc]
  229. initWithStatusSuccessAndExpirationDate:token.expirationDate
  230. receivedAtDate:token.receivedAtDate];
  231. completion(refreshResult);
  232. return nil;
  233. })
  234. .catch(^(NSError *error) {
  235. __auto_type refreshResult = [[GACAppCheckTokenRefreshResult alloc] initWithStatusFailure];
  236. completion(refreshResult);
  237. });
  238. }
  239. @end
  240. NS_ASSUME_NONNULL_END