Skip to content

Commit c6cd505

Browse files
apettamikehardy
andauthored
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 <[email protected]>
1 parent c368a82 commit c6cd505

File tree

9 files changed

+155
-24
lines changed

9 files changed

+155
-24
lines changed

packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,35 @@ public void getToken(String appName, boolean forceRefresh, Promise promise) {
206206
});
207207
}
208208

209+
@ReactMethod
210+
public void getLimitedUseToken(String appName, Promise promise) {
211+
Log.d(LOGTAG, "getLimitedUseToken appName: " + appName);
212+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
213+
214+
Tasks.call(
215+
getExecutor(),
216+
() -> {
217+
return Tasks.await(
218+
FirebaseAppCheck.getInstance(firebaseApp).getLimitedUseAppCheckToken());
219+
})
220+
.addOnCompleteListener(
221+
getExecutor(),
222+
(task) -> {
223+
if (task.isSuccessful()) {
224+
WritableMap tokenResultMap = Arguments.createMap();
225+
tokenResultMap.putString("token", task.getResult().getToken());
226+
promise.resolve(tokenResultMap);
227+
} else {
228+
Log.e(
229+
LOGTAG,
230+
"Unknown error while fetching limited-use AppCheck token "
231+
+ task.getException().getMessage());
232+
rejectPromiseWithCodeAndMessage(
233+
promise, "token-error", task.getException().getMessage());
234+
}
235+
});
236+
}
237+
209238
/** Add a new token change listener - if one doesn't exist already */
210239
@ReactMethod
211240
public void addAppCheckListener(final String appName) {

packages/app-check/e2e/appcheck.e2e.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,18 @@ describe('appCheck() modular', function () {
156156
});
157157
});
158158

159+
describe('getLimitedUseToken())', function () {
160+
it('limited use token fetch attempt with configured debug token should work', async function () {
161+
const { token } = await firebase.appCheck().getLimitedUseToken();
162+
token.should.not.equal('');
163+
const decodedToken = jwt.decode(token);
164+
decodedToken.aud[1].should.equal('projects/react-native-firebase-testing');
165+
if (decodedToken.exp < Date.now()) {
166+
Promise.reject('Token already expired');
167+
}
168+
});
169+
});
170+
159171
describe('activate())', function () {
160172
it('should activate with default provider and defined token refresh', function () {
161173
firebase
@@ -317,5 +329,39 @@ describe('appCheck() modular', function () {
317329
}
318330
});
319331
});
332+
333+
describe('getLimitedUseToken())', function () {
334+
it('limited use token fetch attempt with configured debug token should work', async function () {
335+
const { initializeAppCheck, getLimitedUseToken } = appCheckModular;
336+
337+
rnfbProvider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider();
338+
rnfbProvider.configure({
339+
android: {
340+
provider: 'debug',
341+
debugToken: '698956B2-187B-49C6-9E25-C3F3530EEBAF',
342+
},
343+
apple: {
344+
provider: 'debug',
345+
},
346+
web: {
347+
provider: 'debug',
348+
siteKey: 'none',
349+
},
350+
});
351+
352+
const appCheckInstance = await initializeAppCheck(undefined, {
353+
provider: rnfbProvider,
354+
isTokenAutoRefreshEnabled: false,
355+
});
356+
357+
const { token } = await getLimitedUseToken(appCheckInstance);
358+
token.should.not.equal('');
359+
const decodedToken = jwt.decode(token);
360+
decodedToken.aud[1].should.equal('projects/react-native-firebase-testing');
361+
if (decodedToken.exp < Date.now()) {
362+
Promise.reject('Token already expired');
363+
}
364+
});
365+
});
320366
});
321367
});

packages/app-check/ios/RNFBAppCheck/RNFBAppCheckDebugProvider.h

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,38 @@ + (instancetype)sharedInstance {
134134
}];
135135
}
136136

137+
RCT_EXPORT_METHOD(getLimitedUseToken
138+
: (FIRApp *)firebaseApp
139+
: (RCTPromiseResolveBlock)resolve
140+
: (RCTPromiseRejectBlock)reject) {
141+
FIRAppCheck *appCheck = [FIRAppCheck appCheckWithApp:firebaseApp];
142+
DLog(@"appName %@", firebaseApp.name);
143+
[appCheck limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token,
144+
NSError *_Nullable error) {
145+
if (error != nil) {
146+
// Handle any errors if the token was not retrieved.
147+
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token: %@", error);
148+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
149+
userInfo:(NSMutableDictionary *)@{
150+
@"code" : @"token-error",
151+
@"message" : [error localizedDescription],
152+
}];
153+
return;
154+
}
155+
if (token == nil) {
156+
DLog(@"RNFBAppCheck - getLimitedUseToken - Unable to retrieve App Check token.");
157+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject
158+
userInfo:(NSMutableDictionary *)@{
159+
@"code" : @"token-null",
160+
@"message" : @"no token fetched",
161+
}];
162+
return;
163+
}
164+
165+
NSMutableDictionary *tokenResultDictionary = [NSMutableDictionary new];
166+
tokenResultDictionary[@"token"] = token.token;
167+
resolve(tokenResultDictionary);
168+
}];
169+
}
170+
137171
@end

packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,14 @@ - (void)configure:(FIRApp *)app
7676

7777
- (void)getTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken *_Nullable,
7878
NSError *_Nullable))handler {
79-
DLog(@"proxying to delegateProvider...");
79+
DLog(@"proxying getTokenWithCompletion to delegateProvider...");
8080
[self.delegateProvider getTokenWithCompletion:handler];
8181
}
8282

83+
- (void)getLimitedUseTokenWithCompletion:(nonnull void (^)(FIRAppCheckToken *_Nullable,
84+
NSError *_Nullable))handler {
85+
DLog(@"proxying getLimitedUseTokenWithCompletion to delegateProvider...");
86+
[self.delegateProvider getLimitedUseTokenWithCompletion:handler];
87+
}
88+
8389
@end

packages/app-check/lib/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ export namespace FirebaseAppCheckTypes {
266266
*/
267267
getToken(forceRefresh?: boolean): Promise<AppCheckTokenResult>;
268268

269+
/**
270+
* Requests a Firebase App Check token. This method should be used only if you need to authorize requests
271+
* to a non-Firebase backend. Returns limited-use tokens that are intended for use with your non-Firebase
272+
* backend endpoints that are protected with Replay Protection (https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection).
273+
* This method does not affect the token generation behavior of the getAppCheckToken() method.
274+
*/
275+
getLimitedUseToken(): Promise<AppCheckTokenResult>;
276+
269277
/**
270278
* Registers a listener to changes in the token state. There can be more
271279
* than one listener registered at the same time for one or more

packages/app-check/lib/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import version from './version';
2929
export {
3030
addTokenListener,
3131
getToken,
32+
getLimitedUseToken,
3233
initializeAppCheck,
3334
setTokenAutoRefreshEnabled,
3435
} from './modular/index';
@@ -141,6 +142,10 @@ class FirebaseAppCheckModule extends FirebaseModule {
141142
}
142143
}
143144

145+
getLimitedUseToken() {
146+
return this.native.getLimitedUseToken();
147+
}
148+
144149
_parseListener(listenerOrObserver) {
145150
return typeof listenerOrObserver === 'object'
146151
? listenerOrObserver.next.bind(listenerOrObserver)

packages/app-check/lib/modular/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ export function getToken(appCheckInstance, forceRefresh) {
4545
return appCheckInstance.app.appCheck().getToken(forceRefresh);
4646
}
4747

48+
/**
49+
* Get a limited-use (consumable) App Check token.
50+
* For use with server calls to firebase functions or custom backends using the firebase admin SDK
51+
* @param appCheckInstance - AppCheck
52+
* @returns {Promise<AppCheckTokenResult>}
53+
*/
54+
export function getLimitedUseToken(appCheckInstance) {
55+
return appCheckInstance.app.appCheck().getLimitedUseToken();
56+
}
57+
4858
/**
4959
* Registers a listener to changes in the token state.
5060
* There can be more than one listener registered at the same time for one or more App Check instances.

tests/e2e/.mocharc.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ module.exports = {
1111
require: 'node_modules/jet/platform/node',
1212
spec: [
1313
'../packages/app/e2e/**/*.e2e.js',
14-
// '../packages/app-check/e2e/**/*.e2e.js',
15-
// '../packages/app-distribution/e2e/**/*.e2e.js',
16-
// '../packages/analytics/e2e/**/*.e2e.js',
17-
// '../packages/auth/e2e/**/*.e2e.js',
18-
// '../packages/crashlytics/e2e/**/*.e2e.js',
19-
// '../packages/database/e2e/**/*.e2e.js',
20-
// '../packages/dynamic-links/e2e/**/*.e2e.js',
21-
// '../packages/firestore/e2e/**/*.e2e.js',
22-
// '../packages/functions/e2e/**/*.e2e.js',
23-
// '../packages/perf/e2e/**/*.e2e.js',
24-
// '../packages/messaging/e2e/**/*.e2e.js',
25-
// '../packages/ml/e2e/**/*.e2e.js',
26-
// '../packages/in-app-messaging/e2e/**/*.e2e.js',
27-
// '../packages/installations/e2e/**/*.e2e.js',
28-
// '../packages/remote-config/e2e/**/*.e2e.js',
29-
// '../packages/storage/e2e/**/*.e2e.js',
14+
'../packages/app-check/e2e/**/*.e2e.js',
15+
'../packages/app-distribution/e2e/**/*.e2e.js',
16+
'../packages/analytics/e2e/**/*.e2e.js',
17+
'../packages/auth/e2e/**/*.e2e.js',
18+
'../packages/crashlytics/e2e/**/*.e2e.js',
19+
'../packages/database/e2e/**/*.e2e.js',
20+
'../packages/dynamic-links/e2e/**/*.e2e.js',
21+
'../packages/firestore/e2e/**/*.e2e.js',
22+
'../packages/functions/e2e/**/*.e2e.js',
23+
'../packages/perf/e2e/**/*.e2e.js',
24+
'../packages/messaging/e2e/**/*.e2e.js',
25+
'../packages/ml/e2e/**/*.e2e.js',
26+
'../packages/in-app-messaging/e2e/**/*.e2e.js',
27+
'../packages/installations/e2e/**/*.e2e.js',
28+
'../packages/remote-config/e2e/**/*.e2e.js',
29+
'../packages/storage/e2e/**/*.e2e.js',
3030
],
3131
};

0 commit comments

Comments
 (0)