FIRAppCheck.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckErrors.h"
  23. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProvider.h"
  24. #import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckProviderFactory.h"
  25. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
  26. #import "FirebaseAppCheck/Sources/Core/FIRAppCheck+Internal.h"
  27. #import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h"
  28. #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h"
  29. #import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h"
  30. #import "FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h"
  31. #import "FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h"
  32. #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h"
  33. #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h"
  34. #import "FirebaseAppCheck/Interop/FIRAppCheckInterop.h"
  35. #import "FirebaseAppCheck/Interop/FIRAppCheckTokenResultInterop.h"
  36. NS_ASSUME_NONNULL_BEGIN
  37. /// A notification with the specified name is sent to the default notification center
  38. /// (`NotificationCenter.default`) each time a Firebase app check token is refreshed.
  39. /// The user info dictionary contains `kFIRAppCheckTokenNotificationKey` and
  40. /// `kFIRAppCheckAppNameNotificationKey` keys.
  41. const NSNotificationName FIRAppCheckAppCheckTokenDidChangeNotification =
  42. @"FIRAppCheckAppCheckTokenDidChangeNotification";
  43. /// `userInfo` key for the `AppCheckToken` in `appCheckTokenRefreshNotification`.
  44. NSString *const kFIRAppCheckTokenNotificationKey = @"FIRAppCheckTokenNotificationKey";
  45. /// `userInfo` key for the `FirebaseApp.name` in `appCheckTokenRefreshNotification`.
  46. NSString *const kFIRAppCheckAppNameNotificationKey = @"FIRAppCheckAppNameNotificationKey";
  47. static id<FIRAppCheckProviderFactory> _providerFactory;
  48. static const NSTimeInterval kTokenExpirationThreshold = 5 * 60; // 5 min.
  49. static NSString *const kDummyFACTokenValue = @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==";
  50. @interface FIRAppCheck () <FIRAppCheckInterop>
  51. @property(class, nullable) id<FIRAppCheckProviderFactory> providerFactory;
  52. @property(nonatomic, readonly) NSString *appName;
  53. @property(nonatomic, readonly) id<FIRAppCheckProvider> appCheckProvider;
  54. @property(nonatomic, readonly) id<FIRAppCheckStorageProtocol> storage;
  55. @property(nonatomic, readonly) NSNotificationCenter *notificationCenter;
  56. @property(nonatomic, readonly) id<FIRAppCheckSettingsProtocol> settings;
  57. @property(nonatomic, readonly, nullable) id<FIRAppCheckTokenRefresherProtocol> tokenRefresher;
  58. @property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;
  59. @property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingLimitedUseTokenPromise;
  60. @end
  61. @implementation FIRAppCheck
  62. #pragma mark - Internal
  63. - (nullable instancetype)initWithApp:(FIRApp *)app {
  64. id<FIRAppCheckProviderFactory> providerFactory = [FIRAppCheck providerFactory];
  65. if (providerFactory == nil) {
  66. FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeProviderFactoryIsMissing,
  67. @"Cannot instantiate `FIRAppCheck` for app: %@ without a provider factory. "
  68. @"Please register a provider factory using "
  69. @"`AppCheck.setAppCheckProviderFactory(_ ,forAppName:)` method.",
  70. app.name);
  71. return nil;
  72. }
  73. id<FIRAppCheckProvider> appCheckProvider = [providerFactory createProviderWithApp:app];
  74. if (appCheckProvider == nil) {
  75. FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeProviderIsMissing,
  76. @"Cannot instantiate `FIRAppCheck` for app: %@ without an app check provider. "
  77. @"Please make sure the provider factory returns a valid app check provider.",
  78. app.name);
  79. return nil;
  80. }
  81. FIRAppCheckSettings *settings =
  82. [[FIRAppCheckSettings alloc] initWithApp:app
  83. userDefault:[NSUserDefaults standardUserDefaults]
  84. mainBundle:[NSBundle mainBundle]];
  85. FIRAppCheckTokenRefreshResult *refreshResult =
  86. [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever];
  87. FIRAppCheckTokenRefresher *tokenRefresher =
  88. [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:refreshResult settings:settings];
  89. FIRAppCheckStorage *storage = [[FIRAppCheckStorage alloc] initWithAppName:app.name
  90. appID:app.options.googleAppID
  91. accessGroup:app.options.appGroupID];
  92. return [self initWithAppName:app.name
  93. appCheckProvider:appCheckProvider
  94. storage:storage
  95. tokenRefresher:tokenRefresher
  96. notificationCenter:NSNotificationCenter.defaultCenter
  97. settings:settings];
  98. }
  99. - (instancetype)initWithAppName:(NSString *)appName
  100. appCheckProvider:(id<FIRAppCheckProvider>)appCheckProvider
  101. storage:(id<FIRAppCheckStorageProtocol>)storage
  102. tokenRefresher:(id<FIRAppCheckTokenRefresherProtocol>)tokenRefresher
  103. notificationCenter:(NSNotificationCenter *)notificationCenter
  104. settings:(id<FIRAppCheckSettingsProtocol>)settings {
  105. self = [super init];
  106. if (self) {
  107. _appName = appName;
  108. _appCheckProvider = appCheckProvider;
  109. _storage = storage;
  110. _tokenRefresher = tokenRefresher;
  111. _notificationCenter = notificationCenter;
  112. _settings = settings;
  113. __auto_type __weak weakSelf = self;
  114. tokenRefresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) {
  115. __auto_type strongSelf = weakSelf;
  116. [strongSelf periodicTokenRefreshWithCompletion:completion];
  117. };
  118. }
  119. return self;
  120. }
  121. #pragma mark - Public
  122. + (instancetype)appCheck {
  123. FIRApp *defaultApp = [FIRApp defaultApp];
  124. if (!defaultApp) {
  125. [NSException raise:FIRAppCheckErrorDomain
  126. format:@"The default FirebaseApp instance must be configured before the default"
  127. @"AppCheck instance can be initialized. One way to ensure this is to "
  128. @"call `FirebaseApp.configure()` in the App Delegate's "
  129. @"`application(_:didFinishLaunchingWithOptions:)` (or the `@main` struct's "
  130. @"initializer in SwiftUI)."];
  131. }
  132. return [self appCheckWithApp:defaultApp];
  133. }
  134. + (nullable instancetype)appCheckWithApp:(FIRApp *)firebaseApp {
  135. id<FIRAppCheckInterop> appCheck = FIR_COMPONENT(FIRAppCheckInterop, firebaseApp.container);
  136. return (FIRAppCheck *)appCheck;
  137. }
  138. - (void)tokenForcingRefresh:(BOOL)forcingRefresh
  139. completion:(void (^)(FIRAppCheckToken *_Nullable token,
  140. NSError *_Nullable error))handler {
  141. [self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
  142. .then(^id _Nullable(FIRAppCheckToken *token) {
  143. handler(token, nil);
  144. return token;
  145. })
  146. .catch(^(NSError *_Nonnull error) {
  147. handler(nil, [FIRAppCheckErrorUtil publicDomainErrorWithError:error]);
  148. });
  149. }
  150. - (void)limitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token,
  151. NSError *_Nullable error))handler {
  152. [self retrieveLimitedUseToken]
  153. .then(^id _Nullable(FIRAppCheckToken *token) {
  154. handler(token, nil);
  155. return token;
  156. })
  157. .catch(^(NSError *_Nonnull error) {
  158. handler(nil, [FIRAppCheckErrorUtil publicDomainErrorWithError:error]);
  159. });
  160. }
  161. + (void)setAppCheckProviderFactory:(nullable id<FIRAppCheckProviderFactory>)factory {
  162. self.providerFactory = factory;
  163. }
  164. - (void)setIsTokenAutoRefreshEnabled:(BOOL)isTokenAutoRefreshEnabled {
  165. self.settings.isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
  166. }
  167. - (BOOL)isTokenAutoRefreshEnabled {
  168. return self.settings.isTokenAutoRefreshEnabled;
  169. }
  170. #pragma mark - App Check Provider Ingestion
  171. + (void)setProviderFactory:(nullable id<FIRAppCheckProviderFactory>)providerFactory {
  172. @synchronized(self) {
  173. _providerFactory = providerFactory;
  174. }
  175. }
  176. + (nullable id<FIRAppCheckProviderFactory>)providerFactory {
  177. @synchronized(self) {
  178. return _providerFactory;
  179. }
  180. }
  181. #pragma mark - FIRAppCheckInterop
  182. - (void)getTokenForcingRefresh:(BOOL)forcingRefresh
  183. completion:(FIRAppCheckTokenHandlerInterop)handler {
  184. [self retrieveOrRefreshTokenForcingRefresh:forcingRefresh]
  185. .then(^id _Nullable(FIRAppCheckToken *token) {
  186. FIRAppCheckTokenResult *result = [[FIRAppCheckTokenResult alloc] initWithToken:token.token
  187. error:nil];
  188. handler(result);
  189. return result;
  190. })
  191. .catch(^(NSError *_Nonnull error) {
  192. FIRAppCheckTokenResult *result =
  193. [[FIRAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
  194. handler(result);
  195. });
  196. }
  197. - (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler {
  198. [self retrieveLimitedUseToken]
  199. .then(^id _Nullable(FIRAppCheckToken *token) {
  200. FIRAppCheckTokenResult *result = [[FIRAppCheckTokenResult alloc] initWithToken:token.token
  201. error:nil];
  202. handler(result);
  203. return result;
  204. })
  205. .catch(^(NSError *_Nonnull error) {
  206. FIRAppCheckTokenResult *result =
  207. [[FIRAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
  208. handler(result);
  209. });
  210. }
  211. - (nonnull NSString *)tokenDidChangeNotificationName {
  212. return FIRAppCheckAppCheckTokenDidChangeNotification;
  213. }
  214. - (nonnull NSString *)notificationAppNameKey {
  215. return kFIRAppCheckAppNameNotificationKey;
  216. }
  217. - (nonnull NSString *)notificationTokenKey {
  218. return kFIRAppCheckTokenNotificationKey;
  219. }
  220. #pragma mark - FAA token cache
  221. - (FBLPromise<FIRAppCheckToken *> *)retrieveOrRefreshTokenForcingRefresh:(BOOL)forcingRefresh {
  222. return [FBLPromise do:^id _Nullable {
  223. if (self.ongoingRetrieveOrRefreshTokenPromise == nil) {
  224. // Kick off a new operation only when there is not an ongoing one.
  225. self.ongoingRetrieveOrRefreshTokenPromise =
  226. [self createRetrieveOrRefreshTokenPromiseForcingRefresh:forcingRefresh]
  227. // Release the ongoing operation promise on completion.
  228. .then(^FIRAppCheckToken *(FIRAppCheckToken *token) {
  229. self.ongoingRetrieveOrRefreshTokenPromise = nil;
  230. return token;
  231. })
  232. .recover(^NSError *(NSError *error) {
  233. self.ongoingRetrieveOrRefreshTokenPromise = nil;
  234. return error;
  235. });
  236. }
  237. return self.ongoingRetrieveOrRefreshTokenPromise;
  238. }];
  239. }
  240. - (FBLPromise<FIRAppCheckToken *> *)createRetrieveOrRefreshTokenPromiseForcingRefresh:
  241. (BOOL)forcingRefresh {
  242. return [self getCachedValidTokenForcingRefresh:forcingRefresh].recover(
  243. ^id _Nullable(NSError *_Nonnull error) {
  244. return [self refreshToken];
  245. });
  246. }
  247. - (FBLPromise<FIRAppCheckToken *> *)getCachedValidTokenForcingRefresh:(BOOL)forcingRefresh {
  248. if (forcingRefresh) {
  249. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  250. [rejectedPromise reject:[FIRAppCheckErrorUtil cachedTokenNotFound]];
  251. return rejectedPromise;
  252. }
  253. return [self.storage getToken].then(^id(FIRAppCheckToken *_Nullable token) {
  254. if (token == nil) {
  255. return [FIRAppCheckErrorUtil cachedTokenNotFound];
  256. }
  257. BOOL isTokenExpiredOrExpiresSoon =
  258. [token.expirationDate timeIntervalSinceNow] < kTokenExpirationThreshold;
  259. if (isTokenExpiredOrExpiresSoon) {
  260. return [FIRAppCheckErrorUtil cachedTokenExpired];
  261. }
  262. return token;
  263. });
  264. }
  265. - (FBLPromise<FIRAppCheckToken *> *)retrieveLimitedUseToken {
  266. return [FBLPromise do:^id _Nullable {
  267. if (self.ongoingLimitedUseTokenPromise == nil) {
  268. // Kick off a new operation only when there is not an ongoing one.
  269. self.ongoingLimitedUseTokenPromise =
  270. [self limitedUseToken]
  271. // Release the ongoing operation promise on completion.
  272. .then(^FIRAppCheckToken *(FIRAppCheckToken *token) {
  273. self.ongoingLimitedUseTokenPromise = nil;
  274. return token;
  275. })
  276. .recover(^NSError *(NSError *error) {
  277. self.ongoingLimitedUseTokenPromise = nil;
  278. return error;
  279. });
  280. }
  281. return self.ongoingLimitedUseTokenPromise;
  282. }];
  283. }
  284. - (FBLPromise<FIRAppCheckToken *> *)refreshToken {
  285. return [FBLPromise
  286. wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
  287. [self.appCheckProvider getTokenWithCompletion:handler];
  288. }]
  289. .then(^id _Nullable(FIRAppCheckToken *_Nullable token) {
  290. return [self.storage setToken:token];
  291. })
  292. .then(^id _Nullable(FIRAppCheckToken *_Nullable token) {
  293. // TODO: Make sure the self.tokenRefresher is updated only once. Currently the timer will be
  294. // updated twice in the case when the refresh triggered by self.tokenRefresher, but it
  295. // should be fine for now as it is a relatively cheap operation.
  296. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc]
  297. initWithStatusSuccessAndExpirationDate:token.expirationDate
  298. receivedAtDate:token.receivedAtDate];
  299. [self.tokenRefresher updateWithRefreshResult:refreshResult];
  300. [self postTokenUpdateNotificationWithToken:token];
  301. return token;
  302. });
  303. }
  304. - (FBLPromise<FIRAppCheckToken *> *)limitedUseToken {
  305. return
  306. [FBLPromise wrapObjectOrErrorCompletion:^(
  307. FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
  308. [self.appCheckProvider getTokenWithCompletion:handler];
  309. }].then(^id _Nullable(FIRAppCheckToken *_Nullable token) {
  310. return token;
  311. });
  312. }
  313. #pragma mark - Token auto refresh
  314. - (void)periodicTokenRefreshWithCompletion:(FIRAppCheckTokenRefreshCompletion)completion {
  315. [self retrieveOrRefreshTokenForcingRefresh:NO]
  316. .then(^id _Nullable(FIRAppCheckToken *_Nullable token) {
  317. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc]
  318. initWithStatusSuccessAndExpirationDate:token.expirationDate
  319. receivedAtDate:token.receivedAtDate];
  320. completion(refreshResult);
  321. return nil;
  322. })
  323. .catch(^(NSError *error) {
  324. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure];
  325. completion(refreshResult);
  326. });
  327. }
  328. #pragma mark - Token update notification
  329. - (void)postTokenUpdateNotificationWithToken:(FIRAppCheckToken *)token {
  330. [self.notificationCenter postNotificationName:FIRAppCheckAppCheckTokenDidChangeNotification
  331. object:self
  332. userInfo:@{
  333. kFIRAppCheckTokenNotificationKey : token.token,
  334. kFIRAppCheckAppNameNotificationKey : self.appName
  335. }];
  336. }
  337. @end
  338. NS_ASSUME_NONNULL_END