diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h index 452532a1b04..52dca974389 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h @@ -58,6 +58,8 @@ @class FIRRevokeTokenResponse; @class FIRGetRecaptchaConfigRequest; @class FIRGetRecaptchaConfigResponse; +@class FIRStartPasskeyEnrollmentRequest; +@class FIRStartPasskeyEnrollmentResponse; @protocol FIRAuthBackendImplementation; @protocol FIRAuthBackendRPCIssuer; @@ -243,15 +245,27 @@ typedef void (^FIRRevokeTokenResponseCallback)(FIRRevokeTokenResponse *_Nullable typedef void (^FIRSignInWithGameCenterResponseCallback)( FIRSignInWithGameCenterResponse *_Nullable response, NSError *_Nullable error); -/** @typedef FIRGetRecaptchaConfigResponseCallback - @brief The type of block used to return the result of a call to the getRecaptchaConfig 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 FIRGetRecaptchaConfigResponseCallback + @brief The type of block used to return the result of a call to the getRecaptchaConfig 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 (^FIRGetRecaptchaConfigResponseCallback)( FIRGetRecaptchaConfigResponse *_Nullable response, NSError *_Nullable error); +/** + @typedef FIRStartPasskeyEnrollmentResponseCallback + @brief The type of block used to return the result of a call to the StartPasskeyEnrollment +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 (^FIRStartPasskeyEnrollmentResponseCallback)( + FIRStartPasskeyEnrollmentResponse *_Nullable response, NSError *_Nullable error); + /** @class FIRAuthBackend @brief Simple static class with methods representing the backend RPCs. @remarks All callback blocks passed as method parameters are invoked asynchronously on the @@ -448,6 +462,17 @@ typedef void (^FIRGetRecaptchaConfigResponseCallback)( #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** @fn startPasskeyEnrollment:callback: + @brief Calls the startPasskeyEnrollment endpoint, which is responsible for receving the + challenge that will later be consumed for platform key creation. + @param request The request parameters. + @param callback The callback. + */ ++ (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request + callback:(FIRStartPasskeyEnrollmentResponseCallback)callback; +#endif + /** @fn revokeToken:callback: @brief Calls the revokeToken endpoint, which is responsible for revoking the given token provided in the request parameters. @@ -622,6 +647,17 @@ typedef void (^FIRGetRecaptchaConfigResponseCallback)( #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** @fn startPasskeyEnrollment:callback: + @brief Calls the startPasskeyEnrollment endpoint, which is responsible for receving the + challenge that will later be consumed for platform key creation. + @param request The request parameters. + @param callback The callback. + */ +- (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request + callback:(FIRStartPasskeyEnrollmentResponseCallback)callback; +#endif + /** @fn revokeToken:callback: @brief Calls the revokeToken endpoint, which is responsible for revoking the given token provided in the request parameters. diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m index 6588611cc1a..53c98e00c0a 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m @@ -58,6 +58,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRSignInWithGameCenterResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSignUpNewUserRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSignUpNewUserResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyClientRequest.h" @@ -673,6 +675,13 @@ + (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callb #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST ++ (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request + callback:(FIRStartPasskeyEnrollmentResponseCallback)callback { + [[self implementation] startPasskeyEnrollment:request callback:callback]; +} +#endif + + (void)revokeToken:(FIRRevokeTokenRequest *)request callback:(FIRRevokeTokenResponseCallback)callback { [[self implementation] revokeToken:request callback:callback]; @@ -1099,6 +1108,22 @@ - (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callb #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +- (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request + callback:(FIRStartPasskeyEnrollmentResponseCallback)callback { + FIRStartPasskeyEnrollmentResponse *response = [[FIRStartPasskeyEnrollmentResponse alloc] init]; + [self callWithRequest:request + response:response + callback:^(NSError *error) { + if (error) { + callback(nil, error); + return; + } + callback(response, nil); + }]; +} +#endif + - (void)revokeToken:(FIRRevokeTokenRequest *)request callback:(FIRRevokeTokenResponseCallback)callback { FIRRevokeTokenResponse *response = [[FIRRevokeTokenResponse alloc] init]; diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h new file mode 100644 index 00000000000..75312439ec6 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h @@ -0,0 +1,38 @@ +/* + * 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/LICENSE2.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 + +/** @class FIRStartPasskeyEnrollmentRequest + @brief Represents the parameters for the startPasskeyEnrollment endpoint. + */ +@interface FIRStartPasskeyEnrollmentRequest : FIRIdentityToolkitRequest + +/** + @property IDToken + @brief The raw user access token + */ +@property(nonatomic, copy, readonly) NSString *IDToken; + +- (nullable instancetype)initWithIDToken:(NSString *)IDToken + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.m new file mode 100644 index 00000000000..17051fcb639 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.m @@ -0,0 +1,67 @@ +/* + * 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/LICENSE2.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/FIRStartPasskeyEnrollmentRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + @var kStartPasskeyEnrollmentEndPoint + @brief GCIP endpoint for startPasskeyEnrollment rpc + */ +static NSString *const kStartPasskeyEnrollmentEndPoint = @"accounts/passkeyEnrollment:start"; + +/** + @var kTenantIDKey + @brief The key for the tenant id value in the request. + */ +static NSString *const kTenantIDKey = @"tenantId"; + +/** + @var kIDToken + @brief The key for idToken value in the request. + */ +static NSString *const kIDToken = @"idToken"; + +@implementation FIRStartPasskeyEnrollmentRequest + +- (nullable instancetype)initWithIDToken:(NSString *)IDToken + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kStartPasskeyEnrollmentEndPoint + requestConfiguration:requestConfiguration]; + + if (self) { + _IDToken = IDToken; + self.useIdentityPlatform = YES; + } + + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error { + NSMutableDictionary *postBody = [NSMutableDictionary dictionary]; + if (_IDToken) { + postBody[kIDToken] = _IDToken; + } + if (self.tenantID) { + postBody[kTenantIDKey] = self.tenantID; + } + return [postBody copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h new file mode 100644 index 00000000000..ddd010a675b --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h @@ -0,0 +1,47 @@ +/* + * 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/LICENSE2.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/FIRAuthRPCResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + @class FIRStartPasskeyEnrollmentResponse + @brief Represents the response from the startPasskeyEnrollment endpoint. + */ +@interface FIRStartPasskeyEnrollmentResponse : NSObject + +/** + @property rpID + @brief The RP ID of the FIDO Relying Party. + */ +@property(nonatomic, readonly, copy) NSString *rpID; + +/** + @property userID + @brief The user ID. + */ +@property(nonatomic, readonly, copy) NSString *userID; + +/** + @property challenge + @brief The FIDO challenge. + */ +@property(nonatomic, readonly, copy) NSString *challenge; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.m new file mode 100644 index 00000000000..aed8975f7bc --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.m @@ -0,0 +1,75 @@ +/* + * 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/LICENSE2.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/FIRStartPasskeyEnrollmentResponse.h" + +/** + @var kOptionsKey + @brief The name of the field in the response JSON for CredentialCreationOptions. + */ +static const NSString *kOptionsKey = @"credentialCreationOptions"; + +/** + @var kRpKey + @brief The name of the field in the response JSON for Relying Party. + */ +static const NSString *kRpKey = @"rp"; + +/** + @var kUserKey + @brief The name of the field in the response JSON for User. + */ +static const NSString *kUserKey = @"user"; + +/** + @var kIDKey + @brief The name of the field in the response JSON for ids. + */ +static const NSString *kIDKey = @"id"; + +/** + @var kChallengeKey + @brief The name of the field in the response JSON for challenge. + */ +static const NSString *kChallengeKey = @"challenge"; + +@implementation FIRStartPasskeyEnrollmentResponse + +- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary + error:(NSError *__autoreleasing _Nullable *_Nullable)error { + if (dictionary[kOptionsKey] == nil) { + return NO; + } + if (dictionary[kOptionsKey][kRpKey] == nil || dictionary[kOptionsKey][kRpKey][kIDKey] == nil) { + return NO; + } + + if (dictionary[kOptionsKey][kUserKey] == nil || + dictionary[kOptionsKey][kUserKey][kIDKey] == nil) { + return NO; + } + + if (dictionary[kOptionsKey][kChallengeKey] == nil) { + return NO; + } + + _rpID = dictionary[kOptionsKey][kRpKey][kIDKey]; + _userID = dictionary[kOptionsKey][kUserKey][kIDKey]; + _challenge = dictionary[kOptionsKey][kChallengeKey]; + return YES; +} + +@end diff --git a/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentRequestTests.m b/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentRequestTests.m new file mode 100644 index 00000000000..c31433a25e8 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentRequestTests.m @@ -0,0 +1,104 @@ +/* + * 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 || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST + +#import + +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h" +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h" +#import "FirebaseAuth/Tests/Unit/FIRFakeBackendRPCIssuer.h" + +/** + @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** + @var kTestFirebaseAppID + @brief Fake Firebase app ID used for testing. + */ +static NSString *const kTestFirebaseAppID = @"appID"; + +/** + @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://identitytoolkit.googleapis.com/v2/accounts/passkeyEnrollment:start?key=APIKey"; + +/** + @var kIDToken + @brief Token representing the user's identity. + */ +static NSString *const kIDToken = @"idToken"; + +/** + @class FIRStartPasskeyEnrollmentRequestTests + @brief Tests for @c FIRStartPasskeyEnrollmentRequest. + */ +@interface FIRStartPasskeyEnrollmentRequestTests : XCTestCase +@end + +@implementation FIRStartPasskeyEnrollmentRequestTests { + /** + @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; + + /** + @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:kTestAPIKey + appID:kTestFirebaseAppID]; +} + +- (void)tearDown { + _RPCIssuer = nil; + _requestConfiguration = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +- (void)testStartPasskeyEnrollmentRequest { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error){ + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDToken], kIDToken); +} + +@end +#endif diff --git a/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentResponseTests.m b/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentResponseTests.m new file mode 100644 index 00000000000..ca3286d3b1c --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRStartPasskeyEnrollmentResponseTests.m @@ -0,0 +1,311 @@ +/* + * 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 || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h" + +#import "FirebaseAuth/Sources/Backend/FIRAuthBackend.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h" +#import "FirebaseAuth/Sources/Utilities/FIRAuthInternalErrors.h" +#import "FirebaseAuth/Tests/Unit/FIRFakeBackendRPCIssuer.h" + +/** + @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** + @var kTestFirebaseAppID + @brief Fake Firebase app ID used for testing. + */ +static NSString *const kTestFirebaseAppID = @"appID"; + +/** + @var kIDToken + @brief Token representing the user's identity. + */ +static NSString *const kIDToken = @"idToken"; + +/** + @var kTestRpID + @brief Fake Relying Party ID used for testing. + */ +static NSString *const kTestRpID = @"1234567890"; + +/** + @var kTestChallenge + @brief Fake challenge used for testing. + */ +static NSString *const kTestChallenge = @"challengebytes"; + +/** + @var kTestUserID + @brief Fake user id used for testing. + */ +static NSString *const kTestUserID = @"user-id"; + +/** + @var kUsersKey + @brief the name of the "users" property in the response. + */ +static NSString *const kUsersKey = @"users"; + +/** + @var kTestRpKey + @brief the name of the "rp" property in the response. + */ +static NSString *const kTestRpKey = @"rp"; + +/** + @var kTestChallengeKey + @brief the name of the "challenge" property in the response. + */ +static NSString *const kTestChallengeKey = @"challenge"; + +/** + @var kTestUserKey + @brief the name of the "user" property in the response. + */ +static NSString *const kTestUserKey = @"user"; + +/** + @var kTestIDKey + @brief the name of the "id" property in the response. + */ +static NSString *const kTestIDKey = @"id"; + +/** + @class FIRStartPasskeyEnrollmentResponseTests + @brief Tests for @c FIRStartPasskeyEnrollmentResponse. + */ +@interface FIRStartPasskeyEnrollmentResponseTests : XCTestCase +@end +@implementation FIRStartPasskeyEnrollmentResponseTests { + /** + @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; + + /** + @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:kTestAPIKey + appID:kTestFirebaseAppID]; +} + +- (void)tearDown { + _RPCIssuer = nil; + _requestConfiguration = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSuccessfulStartPasskeyEnrollmentResponse + @brief This test simulates a successful @c StartPasskeyEnrollment flow. + */ +- (void)testSuccessfulStartPasskeyEnrollmentResponse { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialCreationOptions" : @{ + kTestChallengeKey : kTestChallenge, + kTestRpKey : @{kTestIDKey : kTestRpID}, + kTestUserKey : @{kTestIDKey : kTestUserID}, + }, + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.rpID, kTestRpID); + XCTAssertEqualObjects(RPCResponse.challenge, kTestChallenge); + XCTAssertEqualObjects(RPCResponse.userID, kTestUserID); +} + +/** @fn testStartPasskeyEnrollmentResponseMissingCreationOptionsError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeyEnrollment flow. + */ +- (void)testStartPasskeyEnrollmentResponseMissingCreationOptionsError { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"wrongkey" : @{}, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn testStartPasskeyEnrollmentResponseMissingRpIdError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeyEnrollment flow. + */ +- (void)testStartPasskeyEnrollmentResponseMissingRpIdError { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialCreationOptions" : @{ + kTestChallengeKey : kTestChallenge, + kTestRpKey : @{}, + kTestUserKey : @{kTestIDKey : kTestUserID}, + }, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn testStartPasskeyEnrollmentResponseMissingUserIdError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeyEnrollment flow. + */ +- (void)testStartPasskeyEnrollmentResponseMissingUserIdError { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialCreationOptions" : @{ + kTestChallengeKey : kTestChallenge, + kTestRpKey : @{kTestIDKey : kTestRpID}, + kTestUserKey : @{}, + }, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn testStartPasskeyEnrollmentResponseMissingChallengeError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeyEnrollment flow. + */ +- (void)testStartPasskeyEnrollmentResponseMissingChallengeError { + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialCreationOptions" : @{ + kTestRpKey : @{kTestIDKey : kTestRpID}, + kTestUserKey : @{kTestIDKey : kTestUserID}, + }, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn errorValidationHelperWithCallbackInvoked:rpcError:rpcResponse: + @brief Helper function to validate the unexpected response returned from server in @c + StartPasskeyEnrollment flow. + */ +- (void)errorValidationHelperWithCallbackInvoked:(BOOL)callbackInvoked + rpcError:(NSError *)RPCError + rpcResponse:(FIRStartPasskeyEnrollmentResponse *)RPCResponse { + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInternalError); + XCTAssertNotNil(RPCError.userInfo[NSUnderlyingErrorKey]); + NSError *underlyingError = RPCError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertNotNil(underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]); + XCTAssertNil(RPCResponse); +} + +@end +#endif