Skip to content

Commit

Permalink
feat(app-check): implement getLimitedUseToken / Replay Protection (#7424
Browse files Browse the repository at this point in the history
)

* 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 <github@mikehardy.net>
  • Loading branch information
apetta and mikehardy committed Nov 28, 2023
1 parent c368a82 commit c6cd505
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
46 changes: 46 additions & 0 deletions packages/app-check/e2e/appcheck.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
}
});
});
});
});

This file was deleted.

34 changes: 34 additions & 0 deletions packages/app-check/ios/RNFBAppCheck/RNFBAppCheckModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 7 additions & 1 deletion packages/app-check/ios/RNFBAppCheck/RNFBAppCheckProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions packages/app-check/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ export namespace FirebaseAppCheckTypes {
*/
getToken(forceRefresh?: boolean): Promise<AppCheckTokenResult>;

/**
* 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<AppCheckTokenResult>;

/**
* 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
Expand Down
5 changes: 5 additions & 0 deletions packages/app-check/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import version from './version';
export {
addTokenListener,
getToken,
getLimitedUseToken,
initializeAppCheck,
setTokenAutoRefreshEnabled,
} from './modular/index';
Expand Down Expand Up @@ -141,6 +142,10 @@ class FirebaseAppCheckModule extends FirebaseModule {
}
}

getLimitedUseToken() {
return this.native.getLimitedUseToken();
}

_parseListener(listenerOrObserver) {
return typeof listenerOrObserver === 'object'
? listenerOrObserver.next.bind(listenerOrObserver)
Expand Down
10 changes: 10 additions & 0 deletions packages/app-check/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppCheckTokenResult>}
*/
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.
Expand Down
32 changes: 16 additions & 16 deletions tests/e2e/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
};

1 comment on commit c6cd505

@vercel
Copy link

@vercel vercel bot commented on c6cd505 Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.