diff --git a/FirebaseAuth/Sources/Auth/FIRAuth.m b/FirebaseAuth/Sources/Auth/FIRAuth.m index dd3bdde2120..15a130b2350 100644 --- a/FirebaseAuth/Sources/Auth/FIRAuth.m +++ b/FirebaseAuth/Sources/Auth/FIRAuth.m @@ -46,6 +46,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSendVerificationCodeRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSendVerificationCodeResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h" @@ -1540,6 +1542,34 @@ - (void)setAdditionalFrameworkMarker:(nullable NSString *)additionalFrameworkMar }); } +- (void)revokeTokenWithAuthorizationCode:(NSString *)authorizationCode + completion:(nullable void (^)(NSError *_Nullable error))completion { + [self.currentUser + getIDTokenWithCompletion:^(NSString *_Nullable idToken, NSError *_Nullable error) { + if (completion) { + if (error) { + completion(error); + return; + } + } + FIRRevokeTokenRequest *request = + [[FIRRevokeTokenRequest alloc] initWithToken:authorizationCode + idToken:idToken + requestConfiguration:self->_requestConfiguration]; + [FIRAuthBackend + revokeToken:request + callback:^(FIRRevokeTokenResponse *_Nullable response, NSError *_Nullable error) { + if (completion) { + if (error) { + completion(error); + } else { + completion(nil); + } + } + }]; + }]; +} + #if TARGET_OS_IOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-property-ivar" diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h index d3e9bca44df..e02cc456459 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h @@ -54,6 +54,8 @@ @class FIRSignInWithGameCenterResponse; @class FIRSignUpNewUserRequest; @class FIRSignUpNewUserResponse; +@class FIRRevokeTokenRequest; +@class FIRRevokeTokenResponse; @protocol FIRAuthBackendImplementation; @protocol FIRAuthBackendRPCIssuer; @@ -220,6 +222,15 @@ typedef void (^FIRVerifyPhoneNumberResponseCallback)( typedef void (^FIRVerifyClientResponseCallback)(FIRVerifyClientResponse *_Nullable response, NSError *_Nullable error); +/** @typedef FIRRevokeTokenResponseCallback + @brief The type of block used to return the result of a call to the revokeToken endpoint. + @param response The received response, if any. + @param error The error which occurred, if any. + @remarks One of response or error will be non-nil. + */ +typedef void (^FIRRevokeTokenResponseCallback)(FIRRevokeTokenResponse *_Nullable response, + NSError *_Nullable error); + /** @typedef FIRSignInWithGameCenterResponseCallback @brief The type of block used to return the result of a call to the SignInWithGameCenter endpoint. @@ -414,8 +425,18 @@ typedef void (^FIRSignInWithGameCenterResponseCallback)( */ + (void)verifyClient:(FIRVerifyClientRequest *)request callback:(FIRVerifyClientResponseCallback)callback; + #endif +/** @fn revokeToken:callback: + @brief Calls the revokeToken endpoint, which is responsible for revoking the given token + provided in the request parameters. + @param request The request parameters. + @param callback The callback. + */ ++ (void)revokeToken:(FIRRevokeTokenRequest *)request + callback:(FIRRevokeTokenResponseCallback)callback; + @end /** @protocol FIRAuthBackendRPCIssuer @@ -578,8 +599,18 @@ typedef void (^FIRSignInWithGameCenterResponseCallback)( */ - (void)verifyClient:(FIRVerifyClientRequest *)request callback:(FIRVerifyClientResponseCallback)callback; + #endif +/** @fn revokeToken:callback: + @brief Calls the revokeToken endpoint, which is responsible for revoking the given token + provided in the request parameters. + @param request The request parameters. + @param callback The callback. + */ +- (void)revokeToken:(FIRRevokeTokenRequest *)request + callback:(FIRRevokeTokenResponseCallback)callback; + /** @fn SignInWithGameCenter:callback: @brief Calls the SignInWithGameCenter endpoint, which is responsible for authenticating a user who has Game Center credentials. diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m index c567b2245dc..8eab01e49e7 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m @@ -41,6 +41,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRGetProjectConfigResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSecureTokenRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSecureTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSendVerificationCodeRequest.h" @@ -603,8 +605,14 @@ + (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request + (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callback { [[self implementation] verifyClient:request callback:callback]; } + #endif ++ (void)revokeToken:(FIRRevokeTokenRequest *)request + callback:(FIRRevokeTokenResponseCallback)callback { + [[self implementation] revokeToken:request callback:callback]; +} + + (void)resetPassword:(FIRResetPasswordRequest *)request callback:(FIRResetPasswordCallback)callback { [[self implementation] resetPassword:request callback:callback]; @@ -967,8 +975,25 @@ - (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callb callback(response, nil); }]; } + #endif +- (void)revokeToken:(FIRRevokeTokenRequest *)request + callback:(FIRRevokeTokenResponseCallback)callback { + FIRRevokeTokenResponse *response = [[FIRRevokeTokenResponse alloc] init]; + [self + postWithRequest:request + response:response + callback:^(NSError *error) { + if (error) { + callback(nil, [FIRAuthErrorUtils + invalidCredentialErrorWithMessage:[error localizedDescription]]); + return; + } + callback(response, nil); + }]; +} + - (void)resetPassword:(FIRResetPasswordRequest *)request callback:(FIRResetPasswordCallback)callback { FIRResetPasswordResponse *response = [[FIRResetPasswordResponse alloc] init]; diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h new file mode 100644 index 00000000000..3d4c7b786b5 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FirebaseAuth/Sources/Backend/FIRAuthRPCRequest.h" +#import "FirebaseAuth/Sources/Backend/FIRIdentityToolkitRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRRevokeTokenRequest : FIRIdentityToolkitRequest + +/** @property providerID + @brief The provider that issued the token to revoke. + */ +@property(nonatomic, copy, nullable) NSString *providerID; + +/** @property tokenType + @brief The type of the token to revoke. + */ +@property(nonatomic) NSInteger tokenType; + +/** @property token + @brief The token to be revoked. + */ +@property(nonatomic, copy, nullable) NSString *token; + +/** @property idToken + @brief The ID Token associated with this credential. + */ +@property(nonatomic, copy, nullable) NSString *idToken; + +/** @fn initWithEndpoint:requestConfiguration: + @brief Please use initWithToken:requestConfiguration: instead. + */ +- (nullable instancetype)initWithEndpoint:(NSString *)endpoint + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration + NS_UNAVAILABLE; + +/** @fn initWithAppToken:isSandbox:requestConfiguration: + @brief Designated initializer. + @param token The token to be revoked. + @param idToken The id token associated with the current user. + @param requestConfiguration An object containing configurations to be added to the request. + */ +- (nullable instancetype)initWithToken:(NSString *)token + idToken:(NSString *)idToken + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.m new file mode 100644 index 00000000000..418329ff1b1 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.m @@ -0,0 +1,103 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @var kRevokeTokenEndpoint + @brief The endpoint for the revokeToken request. + */ +static NSString *const kRevokeTokenEndpoint = @"accounts:revokeToken"; + +/** @var kProviderIDKey + @brief The key for the provider that issued the token to revoke. + */ +static NSString *const kProviderIDKey = @"providerId"; + +/** @var kTokenTypeKey + @brief The key for the type of the token to revoke. + */ +static NSString *const kTokenTypeKey = @"tokenType"; + +/** @var kTokenKey + @brief The key for the token to be revoked. + */ +static NSString *const kTokenKey = @"token"; + +/** @var kIDTokenKey + @brief The key for the ID Token associated with this credential. + */ +static NSString *const kIDTokenKey = @"idToken"; + +typedef NS_ENUM(NSInteger, FIRTokenType) { + /** Indicates that the token type is unspecified. + */ + FIRTokenTypeUnspecified = 0, + + /** Indicates that the token type is refresh token. + */ + FIRTokenTypeRefreshToken = 1, + + /** Indicates that the token type is access token. + */ + FIRTokenTypeAccessToken = 2, + + /** Indicates that the token type is authorization code. + */ + FIRTokenTypeAuthorizationCode = 3, +}; + +@implementation FIRRevokeTokenRequest + +- (nullable instancetype)initWithToken:(NSString *)token + idToken:(NSString *)idToken + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kRevokeTokenEndpoint + requestConfiguration:requestConfiguration + useIdentityPlatform:YES + useStaging:NO]; + if (self) { + // Apple and authorization code are the only provider and token type we support for now. + // Generalize this initializer to accept other providers and token types once supported. + _providerID = @"apple.com"; + _tokenType = FIRTokenTypeAuthorizationCode; + _token = token; + _idToken = idToken; + } + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error { + NSMutableDictionary *postBody = [NSMutableDictionary dictionary]; + if (_providerID) { + postBody[kProviderIDKey] = _providerID; + } + if (_tokenType) { + postBody[kTokenTypeKey] = [NSNumber numberWithInteger:_tokenType].stringValue; + } + if (_token) { + postBody[kTokenKey] = _token; + } + if (_idToken) { + postBody[kIDTokenKey] = _idToken; + } + return [postBody copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h new file mode 100644 index 00000000000..270ec534eed --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRRevokeTokenResponse : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.m new file mode 100644 index 00000000000..328e66f01ff --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.m @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRRevokeTokenResponse + +- (BOOL)setWithDictionary:(NSDictionary *)dictionary error:(NSError *_Nullable *_Nullable)error { + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h index caa21187231..9c3927d9157 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h @@ -850,6 +850,14 @@ NS_SWIFT_NAME(Auth) */ - (BOOL)canHandleNotification:(NSDictionary *)userInfo API_UNAVAILABLE(macos, tvos, watchos); +/** @fn revokeTokenWithAuthorizationCode:Completion + @brief Revoke the users token with authorization code. + @param completion (Optional) the block invoked when the request to revoke the token is + complete, or fails. Invoked asynchronously on the main thread in the future. + */ +- (void)revokeTokenWithAuthorizationCode:(NSString *)authorizationCode + completion:(nullable void (^)(NSError *_Nullable error))completion; + #pragma mark - User sharing /** @fn useUserAccessGroup:error: diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m index 03d47cb1af5..194a54064c4 100644 --- a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m @@ -49,6 +49,8 @@ - (StaticContentTableViewSection *)oAuthSection { action:^{ [weakSelf unlinkFromProvider:@"apple.com" completion:nil]; }], [StaticContentTableViewCell cellWithTitle:@"Reauthenticate with Apple" action:^{ [weakSelf reauthenticateWithApple]; }], + [StaticContentTableViewCell cellWithTitle:@"Revoke Apple Token and Delete User" + action:^{ [weakSelf revokeAppleTokenAndDeleteUser]; }], [StaticContentTableViewCell cellWithTitle:@"Sign in with Twitter" action:^{ [weakSelf signInTwitterHeadfulLite]; }], [StaticContentTableViewCell cellWithTitle:@"Sign in with GitHub" @@ -407,6 +409,30 @@ - (void)reauthenticateWithApple { } } +- (void)revokeAppleTokenAndDeleteUser { + FIRUser *user = [FIRAuth auth].currentUser; + + // Optionally revoke Apple token before account deletion + BOOL isAppleProviderLinked = false; + for (id provider in user.providerData) { + if ([[provider providerID] isEqual: @"apple.com"]) { + isAppleProviderLinked = true; + } + } + if (isAppleProviderLinked) { + if (@available(iOS 13, *)) { + ASAuthorizationAppleIDRequest* request = [self appleIDRequestWithState:@"revokeAppleTokenAndDeleteUser"]; + ASAuthorizationController* controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]]; + controller.delegate = self; + controller.presentationContextProvider = self; + [controller performRequests]; + + } else { + // Usual user deletion + } + } +} + - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) { ASAuthorizationAppleIDCredential* appleIDCredential = authorization.credential; NSString *IDToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]]; @@ -439,7 +465,23 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl NSLog(@"%@", error.description); } }]; - } + } else if ([appleIDCredential.state isEqualToString:@"revokeAppleTokenAndDeleteUser"]) { + NSString *code = [[NSString alloc] initWithData:appleIDCredential.authorizationCode encoding:NSUTF8StringEncoding]; + FIRUser *user = FIRAuth.auth.currentUser; + [FIRAuth.auth revokeTokenWithAuthorizationCode:code completion:^(NSError * _Nullable error) { + if (!error) { + // Token revocation succeeded then delete user again. + [user deleteWithCompletion:^(NSError *_Nullable error) { + if (error) { + [self logFailure:@"delete account failed" error:error]; + } + [self showTypicalUIForUserUpdateResultsWithTitle:@"Delete User" error:error]; + }]; + } else { + NSLog(@"%@", error.description); + } + }]; + } } - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) { diff --git a/FirebaseAuth/Tests/Unit/FIRAuthTests.m b/FirebaseAuth/Tests/Unit/FIRAuthTests.m index 22741b583cc..bd8967b4292 100644 --- a/FirebaseAuth/Tests/Unit/FIRAuthTests.m +++ b/FirebaseAuth/Tests/Unit/FIRAuthTests.m @@ -45,6 +45,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSecureTokenRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSecureTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h" @@ -2773,4 +2775,113 @@ - (void)waitForAuthGlobalWorkQueueDrain { DISPATCH_TIME_FOREVER /*DISPATCH_TIME_NOW + 10 * NSEC_PER_SEC*/); } +/** @fn testRevokeTokenSuccess + @brief Tests the flow of a successful @c revokeToken:completion. + */ +- (void)testRevokeTokenSuccess { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kFakePassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVerifyPasswordResponse]; + callback(mockVerifyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *signInExpectation = [self expectationWithDescription:@"signIn"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail + password:kFakePassword + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + [self assertUser:result.user]; + XCTAssertNil(error); + [signInExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + id mockSecureTokenResponse = OCMClassMock([FIRSecureTokenResponse class]); + OCMStub([mockSecureTokenResponse accessToken]).andReturn(@"IDToken"); + OCMExpect([self->_mockBackend secureToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2( + ^(FIRSecureTokenRequest *_Nullable request, FIRSecureTokenResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(mockSecureTokenResponse, nil); + }); + }); + OCMExpect([_mockBackend revokeToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2( + ^(FIRRevokeTokenRequest *_Nullable request, FIRRevokeTokenResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockRevokeTokenResponse = OCMClassMock([FIRRevokeTokenResponse class]); + callback(mockRevokeTokenResponse, nil); + }); + }); + XCTestExpectation *revokeExpectation = [self expectationWithDescription:@"callback"]; + NSString *code = @"code"; + [[FIRAuth auth] revokeTokenWithAuthorizationCode:code + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [revokeExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testRevokeTokenMissingCallback + @brief Tests the flow of @c revokeToken:completion with a nil callback. + */ +- (void)testRevokeTokenMissingCallback { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kFakePassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVerifyPasswordResponse]; + callback(mockVerifyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *signInExpectation = [self expectationWithDescription:@"signIn"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail + password:kFakePassword + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + [self assertUser:result.user]; + XCTAssertNil(error); + [signInExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + id mockSecureTokenResponse = OCMClassMock([FIRSecureTokenResponse class]); + OCMStub([mockSecureTokenResponse accessToken]).andReturn(@"IDToken"); + OCMExpect([self->_mockBackend secureToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2( + ^(FIRSecureTokenRequest *_Nullable request, FIRSecureTokenResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(mockSecureTokenResponse, nil); + }); + }); + XCTestExpectation *revokeExpectation = [self expectationWithDescription:@"revoke"]; + OCMExpect([_mockBackend revokeToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2( + ^(FIRRevokeTokenRequest *_Nullable request, FIRRevokeTokenResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockRevokeTokenResponse = OCMClassMock([FIRRevokeTokenResponse class]); + callback(mockRevokeTokenResponse, nil); + [revokeExpectation fulfill]; + }); + }); + NSString *code = @"code"; + [[FIRAuth auth] revokeTokenWithAuthorizationCode:code completion:nil]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + @end diff --git a/FirebaseAuth/Tests/Unit/FIRRevokeTokenRequestTests.m b/FirebaseAuth/Tests/Unit/FIRRevokeTokenRequestTests.m new file mode 100644 index 00000000000..06570337026 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRRevokeTokenRequestTests.m @@ -0,0 +1,124 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#if TARGET_OS_IOS + +#import + +#import "FirebaseAuth/Sources/Backend/FIRAuthBackend.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" +#import "FirebaseAuth/Tests/Unit/FIRFakeBackendRPCIssuer.h" + +/** @var kFakeToken + @brief The fake token to use in the test request. + */ +static NSString *const kFakeTokenKey = @"token"; + +/** @var kFakeToken + @brief The fake token to use in the test request. + */ +static NSString *const kFakeToken = @"fakeToken"; + +/** @var kFakeIDToken + @brief The fake ID token to use in the test request. + */ +static NSString *const kFakeIDTokenKey = @"idToken"; + +/** @var kFakeIDToken + @brief The fake ID token to use in the test request. + */ +static NSString *const kFakeIDToken = @"fakeIDToken"; + +/** @var kFakeProviderIDKey + @brief The fake provider id key to use in the test request. + */ +static NSString *const kFakeProviderIDKey = @"providerId"; + +/** @var kFakeTokenTypeKey + @brief The fake ID token to use in the test request. + */ +static NSString *const kFakeTokenTypeKey = @"tokenType"; + +/** @var kFakeAPIKey + @brief The fake API key to use in the test request. + */ +static NSString *const kFakeAPIKey = @"APIKey"; + +/** @var kFakeFirebaseAppID + @brief The fake Firebase app ID to use in the test request. + */ +static NSString *const kFakeFirebaseAppID = @"appID"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://identitytoolkit.googleapis.com/v2/accounts:revokeToken?key=APIKey"; + +/** @class FIRRevokeTokenRequestTest + @brief Tests for @c FIRRevokeTokenRequests. + */ +@interface FIRRevokeTokenRequestTest : XCTestCase +@end + +@implementation FIRRevokeTokenRequestTest { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testRevokeTokenRequest + @brief Tests the token revocation request. + */ +- (void)testRevokeTokenRequest { + FIRAuthRequestConfiguration *requestConfiguration = + [[FIRAuthRequestConfiguration alloc] initWithAPIKey:kFakeAPIKey appID:kFakeFirebaseAppID]; + FIRRevokeTokenRequest *request = + [[FIRRevokeTokenRequest alloc] initWithToken:kFakeToken + idToken:kFakeIDToken + requestConfiguration:requestConfiguration]; + [FIRAuthBackend + revokeToken:request + callback:^(FIRRevokeTokenResponse *_Nullable response, NSError *_Nullable error){ + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kFakeIDTokenKey], kFakeIDToken); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kFakeTokenKey], kFakeToken); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kFakeProviderIDKey], @"apple.com"); + XCTAssertEqual([_RPCIssuer.decodedRequest[kFakeTokenTypeKey] intValue], 3); +} + +@end + +#endif diff --git a/FirebaseAuth/Tests/Unit/FIRRevokeTokenResponseTests.m b/FirebaseAuth/Tests/Unit/FIRRevokeTokenResponseTests.m new file mode 100644 index 00000000000..578353db2b9 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRRevokeTokenResponseTests.m @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#if TARGET_OS_IOS + +#import + +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h" + +#import "FirebaseAuth/Sources/Backend/FIRAuthBackend.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRRevokeTokenResponse.h" +#import "FirebaseAuth/Tests/Unit/FIRFakeBackendRPCIssuer.h" + +/** @var kFakeToken + @brief The fake token to use in the test request. + */ +static NSString *const kFakeToken = @"fakeToken"; + +/** @var kFakeIDToken + @brief The fake ID token to use in the test request. + */ +static NSString *const kFakeIDToken = @"fakeIDToken"; + +/** @var kFakeToken + @brief The fake token to use in the test request. + */ +static NSString *const kFakeTokenKey = @"tokenKey"; + +/** @var kFakeIDToken + @brief The fake ID token to use in the test request. + */ +static NSString *const kFakeIDTokenKey = @"idTokenKey"; + +/** @var kFakeAPIKey + @brief The fake API key to use in the test request. + */ +static NSString *const kFakeAPIKey = @"APIKey"; + +/** @var kFakeFirebaseAppID + @brief The fake Firebase app ID to use in the test request. + */ +static NSString *const kFakeFirebaseAppID = @"appID"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/revokeToken?key=APIKey"; + +@interface FIRRevokeTokenResponseTests : XCTestCase +@end + +@implementation FIRRevokeTokenResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; + + /** @var _requestConfiguration + @brief This is the request configuration used for testing. + */ + FIRAuthRequestConfiguration *_requestConfiguration; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; + _requestConfiguration = [[FIRAuthRequestConfiguration alloc] initWithAPIKey:kFakeAPIKey + appID:kFakeFirebaseAppID]; +} + +/** @fn testSuccessfulRevokeTokenResponse + @brief Tests a succesful attempt of the token revocation flow. + */ +- (void)testSuccessfulResponse { + FIRRevokeTokenRequest *request = + [[FIRRevokeTokenRequest alloc] initWithToken:kFakeToken + idToken:kFakeIDToken + requestConfiguration:_requestConfiguration]; + __block BOOL callbackInvoked; + __block FIRRevokeTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend + revokeToken:request + callback:^(FIRRevokeTokenResponse *_Nullable response, NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{}]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCResponse); +} + +@end + +#endif