From c6cd50501a09855fe7253873549eabe869a24978 Mon Sep 17 00:00:00 2001 From: apetta Date: Tue, 28 Nov 2023 11:40:52 -0800 Subject: [PATCH] feat(app-check): implement getLimitedUseToken / Replay Protection (#7424) * feat(app-check): Replay Protection * chore(app-check): delete unused file this was a vestige of original implementation plan for the iOS custom app check provider, unused in the end * test: fix inadvertently commented out tests * test(app-check): add e2e test for getLimitedUseToken * fix(app-check, ios): proxy getLimitedUseToken to current delegate provider * fix(app-check, types): add getLimitedUseToken API to typescript defs * style(lint): `yarn lint:android && yarn lint:ios:fix` --------- Co-authored-by: Mike Hardy --- .../ReactNativeFirebaseAppCheckModule.java | 29 ++++++++++++ packages/app-check/e2e/appcheck.e2e.js | 46 +++++++++++++++++++ .../RNFBAppCheck/RNFBAppCheckDebugProvider.h | 7 --- .../ios/RNFBAppCheck/RNFBAppCheckModule.m | 34 ++++++++++++++ .../ios/RNFBAppCheck/RNFBAppCheckProvider.m | 8 +++- packages/app-check/lib/index.d.ts | 8 ++++ packages/app-check/lib/index.js | 5 ++ packages/app-check/lib/modular/index.js | 10 ++++ tests/e2e/.mocharc.js | 32 ++++++------- 9 files changed, 155 insertions(+), 24 deletions(-) delete mode 100644 packages/app-check/ios/RNFBAppCheck/RNFBAppCheckDebugProvider.h diff --git a/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java b/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java index 15e918e7c5..9789917cf6 100644 --- a/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java +++ b/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java @@ -206,6 +206,35 @@ public void getToken(String appName, boolean forceRefresh, Promise promise) { }); } + @ReactMethod + public void getLimitedUseToken(String appName, Promise promise) { + Log.d(LOGTAG, "getLimitedUseToken appName: " + appName); + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + + Tasks.call( + getExecutor(), + () -> { + return Tasks.await( + FirebaseAppCheck.getInstance(firebaseApp).getLimitedUseAppCheckToken()); + }) + .addOnCompleteListener( + getExecutor(), + (task) -> { + if (task.isSuccessful()) { + WritableMap tokenResultMap = Arguments.createMap(); + tokenResultMap.putString("token", task.getResult().getToken()); + promise.resolve(tokenResultMap); + } else { + Log.e( + LOGTAG, + "Unknown error while fetching limited-use AppCheck token " + + task.getException().getMessage()); + rejectPromiseWithCodeAndMessage( + promise, "token-error", task.getException().getMessage()); + } + }); + } + /** Add a new token change listener - if one doesn't exist already */ @ReactMethod public void addAppCheckListener(final String appName) { diff --git a/packages/app-check/e2e/appcheck.e2e.js b/packages/app-check/e2e/appcheck.e2e.js index cc6125eb37..f2119e8531 100644 --- a/packages/app-check/e2e/appcheck.e2e.js +++ b/packages/app-check/e2e/appcheck.e2e.js @@ -156,6 +156,18 @@ describe('appCheck() modular', function () { }); }); + describe('getLimitedUseToken())', function () { + it('limited use token fetch attempt with configured debug token should work', async function () { + const { token } = await firebase.appCheck().getLimitedUseToken(); + token.should.not.equal(''); + const decodedToken = jwt.decode(token); + decodedToken.aud[1].should.equal('projects/react-native-firebase-testing'); + if (decodedToken.exp < Date.now()) { + Promise.reject('Token already expired'); + } + }); + }); + describe('activate())', function () { it('should activate with default provider and defined token refresh', function () { firebase @@ -317,5 +329,39 @@ describe('appCheck() modular', function () { } }); }); + + describe('getLimitedUseToken())', function () { + it('limited use token fetch attempt with configured debug token should work', async function () { + const { initializeAppCheck, getLimitedUseToken } = appCheckModular; + + rnfbProvider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider(); + rnfbProvider.configure({ + android: { + provider: 'debug', + debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF', + }, + apple: { + provider: 'debug', + }, + web: { + provider: 'debug', + siteKey: 'none', + }, + }); + + const appCheckInstance = await initializeAppCheck(undefined, { + provider: rnfbProvider, + isTokenAutoRefreshEnabled: false, + }); + + const { token } = await getLimitedUseToken(appCheckInstance); + token.should.not.equal(''); + const decodedToken = jwt.decode(token); + decodedToken.aud[1].should.equal('projects/react-native-firebase-testing'); + if (decodedToken.exp < Date.now()) { + Promise.reject('Token already expired'); + } + }); + }); }); }); diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckDebugProvider.h b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckDebugProvider.h deleted file mode 100644 index 82a496cf77..0000000000 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckDebugProvider.h +++ /dev/null @@ -1,7 +0,0 @@ -// Subclass FIRAppCheckDebugProvider here -// https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MethodOverriding.html - -// 1. have a new nullable configuredDebugToken property -// 2. override implementation of currentDebutToken to: -// - return configuredDebugToken if it exists, -// - else return [super currentDebugToken] if not diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m index f962a7cb69..90530de399 100644 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m +++ b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m @@ -134,4 +134,38 @@ + (instancetype)sharedInstance { }]; } +RCT_EXPORT_METHOD(getLimitedUseToken + : (FIRApp *)firebaseApp + : (RCTPromiseResolveBlock)resolve + : (RCTPromiseRejectBlock)reject) { + FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp]; + DLog(@"appName %@", firebaseApp.name); + [appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error) { + if (error != nil) { + // Handle any errors if the token was not retrieved. + DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-error", + @"message" : [error localizedDescription], + }]; + return; + } + if (token == nil) { + DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token."); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"token-null", + @"message" : @"no token fetched", + }]; + return; + } + + NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new]; + tokenResultDictionary[@"token"] = token.token; + resolve(tokenResultDictionary); + }]; +} + @end diff --git a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m index f68f4ca131..6722366e4c 100644 --- a/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m +++ b/packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m @@ -76,8 +76,14 @@ - (void)configure:(FIRApp *)app - (void)getTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken *_Nullable, NSError *_Nullable))handler { - DLog(@"proxying to delegateProvider..."); + DLog(@"proxying getTokenWithCompletion to delegateProvider..."); [self.delegateProvider getTokenWithCompletion:handler]; } +- (void)getLimitedUseTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken *_Nullable, + NSError *_Nullable))handler { + DLog(@"proxying getLimitedUseTokenWithCompletion to delegateProvider..."); + [self.delegateProvider getLimitedUseTokenWithCompletion:handler]; +} + @end diff --git a/packages/app-check/lib/index.d.ts b/packages/app-check/lib/index.d.ts index 329da83b1a..543b484cc0 100644 --- a/packages/app-check/lib/index.d.ts +++ b/packages/app-check/lib/index.d.ts @@ -266,6 +266,14 @@ export namespace FirebaseAppCheckTypes { */ getToken(forceRefresh?: boolean): Promise; + /** + * Requests a Firebase App Check token. This method should be used only if you need to authorize requests + * to a non-Firebase backend. Returns limited-use tokens that are intended for use with your non-Firebase + * backend endpoints that are protected with Replay Protection (https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection). + * This method does not affect the token generation behavior of the getAppCheckToken() method. + */ + getLimitedUseToken(): Promise; + /** * Registers a listener to changes in the token state. There can be more * than one listener registered at the same time for one or more diff --git a/packages/app-check/lib/index.js b/packages/app-check/lib/index.js index b68734ad6a..cecb4153f8 100644 --- a/packages/app-check/lib/index.js +++ b/packages/app-check/lib/index.js @@ -29,6 +29,7 @@ import version from './version'; export { addTokenListener, getToken, + getLimitedUseToken, initializeAppCheck, setTokenAutoRefreshEnabled, } from './modular/index'; @@ -141,6 +142,10 @@ class FirebaseAppCheckModule extends FirebaseModule { } } + getLimitedUseToken() { + return this.native.getLimitedUseToken(); + } + _parseListener(listenerOrObserver) { return typeof listenerOrObserver === 'object' ? listenerOrObserver.next.bind(listenerOrObserver) diff --git a/packages/app-check/lib/modular/index.js b/packages/app-check/lib/modular/index.js index 3da292967f..928c77f2fa 100644 --- a/packages/app-check/lib/modular/index.js +++ b/packages/app-check/lib/modular/index.js @@ -45,6 +45,16 @@ export function getToken(appCheckInstance, forceRefresh) { return appCheckInstance.app.appCheck().getToken(forceRefresh); } +/** + * Get a limited-use (consumable) App Check token. + * For use with server calls to firebase functions or custom backends using the firebase admin SDK + * @param appCheckInstance - AppCheck + * @returns {Promise} + */ +export function getLimitedUseToken(appCheckInstance) { + return appCheckInstance.app.appCheck().getLimitedUseToken(); +} + /** * Registers a listener to changes in the token state. * There can be more than one listener registered at the same time for one or more App Check instances. diff --git a/tests/e2e/.mocharc.js b/tests/e2e/.mocharc.js index 8a3143335e..580d2173fa 100644 --- a/tests/e2e/.mocharc.js +++ b/tests/e2e/.mocharc.js @@ -11,21 +11,21 @@ module.exports = { require: 'node_modules/jet/platform/node', spec: [ '../packages/app/e2e/**/*.e2e.js', - // '../packages/app-check/e2e/**/*.e2e.js', - // '../packages/app-distribution/e2e/**/*.e2e.js', - // '../packages/analytics/e2e/**/*.e2e.js', - // '../packages/auth/e2e/**/*.e2e.js', - // '../packages/crashlytics/e2e/**/*.e2e.js', - // '../packages/database/e2e/**/*.e2e.js', - // '../packages/dynamic-links/e2e/**/*.e2e.js', - // '../packages/firestore/e2e/**/*.e2e.js', - // '../packages/functions/e2e/**/*.e2e.js', - // '../packages/perf/e2e/**/*.e2e.js', - // '../packages/messaging/e2e/**/*.e2e.js', - // '../packages/ml/e2e/**/*.e2e.js', - // '../packages/in-app-messaging/e2e/**/*.e2e.js', - // '../packages/installations/e2e/**/*.e2e.js', - // '../packages/remote-config/e2e/**/*.e2e.js', - // '../packages/storage/e2e/**/*.e2e.js', + '../packages/app-check/e2e/**/*.e2e.js', + '../packages/app-distribution/e2e/**/*.e2e.js', + '../packages/analytics/e2e/**/*.e2e.js', + '../packages/auth/e2e/**/*.e2e.js', + '../packages/crashlytics/e2e/**/*.e2e.js', + '../packages/database/e2e/**/*.e2e.js', + '../packages/dynamic-links/e2e/**/*.e2e.js', + '../packages/firestore/e2e/**/*.e2e.js', + '../packages/functions/e2e/**/*.e2e.js', + '../packages/perf/e2e/**/*.e2e.js', + '../packages/messaging/e2e/**/*.e2e.js', + '../packages/ml/e2e/**/*.e2e.js', + '../packages/in-app-messaging/e2e/**/*.e2e.js', + '../packages/installations/e2e/**/*.e2e.js', + '../packages/remote-config/e2e/**/*.e2e.js', + '../packages/storage/e2e/**/*.e2e.js', ], };