Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication
.LaunchOptionsKey: Any]?) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)

FirebaseApp.configure()

requestLimitedUseToken()

requestDeviceCheckToken()

requestDebugToken()
Expand Down Expand Up @@ -75,6 +80,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

func requestLimitedUseToken() {
AppCheck.appCheck().limitedUseToken { result, error in
if let result = result {
print("FAC limited-use token: \(result.token), expiration date: \(result.expirationDate)")
}

if let error = error {
print("Error: \(String(describing: error))")
}
}
}

func requestDebugToken() {
guard let firebaseApp = FirebaseApp.app() else {
return
Expand Down
3 changes: 3 additions & 0 deletions FirebaseAppCheck/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#Unreleased
- [feature] Added limitedUseTokenWithCompletion() for obtaining limited-use tokens for protecting non-Firebase backends.

# 9.5.0
- [added] DeviceCheck and App Attest providers are supported by watchOS 9.0+. (#10094, #10098)
- [added] App Attest provider availability updated to support tvOS 15.0+. (#10093)
Expand Down
44 changes: 43 additions & 1 deletion FirebaseAppCheck/Sources/Core/FIRAppCheck.m
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ @interface FIRAppCheck () <FIRLibrary, FIRAppCheckInterop>
@property(nonatomic, readonly, nullable) id<FIRAppCheckTokenRefresherProtocol> tokenRefresher;

@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;

@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingLimitedUseTokenPromise;
@end

@implementation FIRAppCheck
Expand Down Expand Up @@ -200,6 +200,18 @@ - (void)tokenForcingRefresh:(BOOL)forcingRefresh
});
}

- (void)limitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token,
NSError *_Nullable error))handler {
[self retrieveLimitedUseToken]
.then(^id _Nullable(FIRAppCheckToken *token) {
handler(token, nil);
return token;
})
.catch(^(NSError *_Nonnull error) {
handler(nil, [FIRAppCheckErrorUtil publicDomainErrorWithError:error]);
});
}

+ (void)setAppCheckProviderFactory:(nullable id<FIRAppCheckProviderFactory>)factory {
self.providerFactory = factory;
}
Expand Down Expand Up @@ -309,6 +321,26 @@ - (nonnull NSString *)notificationTokenKey {
});
}

- (FBLPromise<FIRAppCheckToken *> *)retrieveLimitedUseToken {
return [FBLPromise do:^id _Nullable {
if (self.ongoingLimitedUseTokenPromise == nil) {
// Kick off a new operation only when there is not an ongoing one.
self.ongoingLimitedUseTokenPromise =
[self limitedUseToken]
// Release the ongoing operation promise on completion.
.then(^FIRAppCheckToken *(FIRAppCheckToken *token) {
self.ongoingLimitedUseTokenPromise = nil;
return token;
})
.recover(^NSError *(NSError *error) {
self.ongoingLimitedUseTokenPromise = nil;
return error;
});
}
return self.ongoingLimitedUseTokenPromise;
}];
}

- (FBLPromise<FIRAppCheckToken *> *)refreshToken {
return [FBLPromise
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
Expand All @@ -330,6 +362,16 @@ - (nonnull NSString *)notificationTokenKey {
});
}

- (FBLPromise<FIRAppCheckToken *> *)limitedUseToken {
return
[FBLPromise wrapObjectOrErrorCompletion:^(
FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
[self.appCheckProvider getTokenWithCompletion:handler];
}].then(^id _Nullable(FIRAppCheckToken *_Nullable token) {
return token;
});
}

#pragma mark - Token auto refresh

- (void)periodicTokenRefreshWithCompletion:(FIRAppCheckTokenRefreshCompletion)completion {
Expand Down
16 changes: 16 additions & 0 deletions FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ NS_SWIFT_NAME(AppCheck)
/// Requests Firebase app check token. This method should *only* be used if you need to authorize
/// requests to a non-Firebase backend. Requests to Firebase backend are authorized automatically if
/// configured.
///
/// If your non-Firebase backend exposes sensitive or expensive endpoints that have low traffic
/// volume, consider protecting it with [Replay
/// Protection](https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection).
/// In this case, use the ``limitedUseTokenWithCompletion()`` instead to obtain a limited-use token.
/// @param forcingRefresh If `YES`, a new Firebase app check token is requested and the token
/// cache is ignored. If `NO`, the cached token is used if it exists and has not expired yet. In
/// most cases, `NO` should be used. `YES` should only be used if the server explicitly returns an
Expand All @@ -65,6 +70,17 @@ NS_SWIFT_NAME(AppCheck)
(void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler
NS_SWIFT_NAME(token(forcingRefresh:completion:));

/// Requests a limited-use 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
/// ``tokenForcingRefresh()`` method.
- (void)limitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token,
NSError *_Nullable error))handler;

/// Sets the `AppCheckProviderFactory` to use to generate
/// `AppCheckDebugProvider` objects.
///
Expand Down
60 changes: 60 additions & 0 deletions FirebaseAppCheck/Tests/Unit/Core/FIRAppCheckTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,66 @@ - (void)testTokenRefreshTriggeredAndRefreshError {
[self verifyAllMocks];
}

- (void)testLimitedUseTokenWithSuccess {
// 1. Don't expect token to be requested from storage.
OCMReject([self.mockStorage getToken]);

// 2. Expect token requested from app check provider.
FIRAppCheckToken *expectedToken = [self validToken];
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);

// 3. Don't expect token requested from storage.
OCMReject([self.mockStorage setToken:expectedToken]);

// 4. Don't expect token update notification to be sent.
XCTestExpectation *notificationExpectation = [self tokenUpdateNotificationWithExpectedToken:@""
isInverted:YES];
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];

[self.appCheck
limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
[getTokenExpectation fulfill];
XCTAssertNotNil(token);
XCTAssertEqualObjects(token.token, expectedToken.token);
XCTAssertNil(error);
}];
[self waitForExpectations:@[ notificationExpectation, getTokenExpectation ] timeout:0.5];
[self verifyAllMocks];
}

- (void)testLimitedUseToken_WhenTokenGenerationErrors {
// 1. Don't expect token to be requested from storage.
OCMReject([self.mockStorage getToken]);

// 2. Expect error when requesting token from app check provider.
NSError *providerError = [FIRAppCheckErrorUtil keychainErrorWithError:[self internalError]];
id completionArg = [OCMArg invokeBlockWithArgs:[NSNull null], providerError, nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);

// 3. Don't expect token requested from app check provider.
OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]);

// 4. Don't expect token update notification to be sent.
XCTestExpectation *notificationExpectation = [self tokenUpdateNotificationWithExpectedToken:@""
isInverted:YES];
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];

[self.appCheck
limitedUseTokenWithCompletion:^(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) {
[getTokenExpectation fulfill];
XCTAssertNotNil(error);
XCTAssertNil(token.token);
XCTAssertEqualObjects(error, providerError);
XCTAssertEqualObjects(error.domain, FIRAppCheckErrorDomain);
}];

[self waitForExpectations:@[ notificationExpectation, getTokenExpectation ] timeout:0.5];
[self verifyAllMocks];
}

#pragma mark - Token update notifications

- (void)testTokenUpdateNotificationKeys {
Expand Down