diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h index 52dca974389a..407a332eba45 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h @@ -60,6 +60,8 @@ @class FIRGetRecaptchaConfigResponse; @class FIRStartPasskeyEnrollmentRequest; @class FIRStartPasskeyEnrollmentResponse; +@class FIRFinalizePasskeyEnrollmentRequest; +@class FIRFinalizePasskeyEnrollmentResponse; @protocol FIRAuthBackendImplementation; @protocol FIRAuthBackendRPCIssuer; @@ -266,6 +268,17 @@ endpoint. typedef void (^FIRStartPasskeyEnrollmentResponseCallback)( FIRStartPasskeyEnrollmentResponse *_Nullable response, NSError *_Nullable error); +/** + @typedef FIRFinalizePasskeyEnrollmentResponseCallback + @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 (^FIRFinalizePasskeyEnrollmentResponseCallback)( + FIRFinalizePasskeyEnrollmentResponse *_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 @@ -471,6 +484,14 @@ typedef void (^FIRStartPasskeyEnrollmentResponseCallback)( */ + (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request callback:(FIRStartPasskeyEnrollmentResponseCallback)callback; + +/** @fn finalizePasskeyEnrollment:callback: + @brief Sends the platform created public info to the finalizePasskeyEnrollment endpoint. + @param request The request parameters. + @param callback The callback. + */ ++ (void)finalizePasskeyEnrollment:(FIRFinalizePasskeyEnrollmentRequest *)request + callback:(FIRFinalizePasskeyEnrollmentResponseCallback)callback; #endif /** @fn revokeToken:callback: @@ -656,6 +677,14 @@ typedef void (^FIRStartPasskeyEnrollmentResponseCallback)( */ - (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request callback:(FIRStartPasskeyEnrollmentResponseCallback)callback; + +/** @fn finalizePasskeyEnrollment:callback: + @brief Calls the finalizePasskeyEnrollment endpoint, which is responsible for sending the platform credential details to GCIP backend to exchange the access token and refresh token. + @param request The request parameters. + @param callback The callback. + */ +- (void)finalizePasskeyEnrollment:(FIRFinalizePasskeyEnrollmentRequest *)request + callback:(FIRFinalizePasskeyEnrollmentResponseCallback)callback; #endif /** @fn revokeToken:callback: diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m index 53c98e00c0a7..f657f1f5cc5f 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m @@ -64,12 +64,16 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyClientRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyClientResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyCustomTokenRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyCustomTokenResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyPasswordRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyPasswordResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyPhoneNumberRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyPhoneNumberResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h" #import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -680,6 +684,10 @@ + (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request callback:(FIRStartPasskeyEnrollmentResponseCallback)callback { [[self implementation] startPasskeyEnrollment:request callback:callback]; } + ++ (void)finalizePasskeyEnrollment:(FIRFinalizePasskeyEnrollmentRequest *)request callback:(FIRFinalizePasskeyEnrollmentResponseCallback)callback { + [[self implementation] finalizePasskeyEnrollment:request callback:callback]; +} #endif + (void)revokeToken:(FIRRevokeTokenRequest *)request @@ -1122,6 +1130,20 @@ - (void)startPasskeyEnrollment:(FIRStartPasskeyEnrollmentRequest *)request callback(response, nil); }]; } + +- (void)finalizePasskeyEnrollment:(FIRFinalizePasskeyEnrollmentRequest *)request callback:(FIRFinalizePasskeyEnrollmentResponseCallback)callback { + FIRFinalizePasskeyEnrollmentResponse *response = [[FIRFinalizePasskeyEnrollmentResponse alloc] init]; + [self callWithRequest:request + response:response + callback:^(NSError *error) { + if (error) { + callback(nil, error); + return; + } + callback(response, nil); + }]; +} + #endif - (void)revokeToken:(FIRRevokeTokenRequest *)request diff --git a/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h b/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h index 0fe981a6eae7..ee1a71fa1822 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h +++ b/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN */ @protocol FIRAuthRPCResponse -/** @fn setFieldsWithDictionary:error: +/** @fn setWithDictionary:error: @brief Sets the response instance from the decoded JSON response. @param dictionary The dictionary decoded from HTTP JSON response. @param error An out field for an error which occurred constructing the request. diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h new file mode 100644 index 000000000000..f8558b14278b --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h @@ -0,0 +1,68 @@ +/* + * 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" +#import + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRFinalizePasskeyEnrollmentRequest + @brief Represents the parameters for the finalizePasskeyEnrollment endpoint. + */ +@interface FIRFinalizePasskeyEnrollmentRequest : FIRIdentityToolkitRequest + +/** + @property IDToken + @brief The raw user access token. + */ +@property(nonatomic, copy, readonly) NSString *IDToken; + +/** + @property name + @brief The passkey name. + */ +@property(nonatomic, copy, readonly) NSString *name; + +/** + @property credentialID + @brief The credential ID. + */ +@property(nonatomic, copy, readonly) NSString *credentialID; + +/** + @property clientDataJson + @brief The CollectedClientData object from the authenticator. + */ +@property(nonatomic, copy, readonly) NSString *clientDataJson; + +/** + + @property attestationObject + @brief The attestation object from the authenticator. + */ +@property(nonatomic, copy, readonly) NSString *attestationObject; + +- (nullable instancetype)initWithIDToken:(NSString *)IDToken + name:(NSString *)name + credentialID:(NSString *)credentialID + clientDataJson:(NSString *)clientDataJson + attestationObject:(NSString *)attestationObject + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.m new file mode 100644 index 000000000000..2cdad6d193a5 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.m @@ -0,0 +1,127 @@ +/* + * 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/FIRFinalizePasskeyEnrollmentRequest.h" +NS_ASSUME_NONNULL_BEGIN + +/** + @var kFinalizePasskeyEnrollmentEndPoint + @brief GCIP endpoint for finalizePasskeyEnrollment rpc + */ +static NSString *const kFinalizePasskeyEnrollmentEndPoint = @"accounts/passkeyEnrollment:finalize"; + +/** + @var kTenantIDKey + @brief The key for the tenant id value in the request. + */ +static NSString *const kTenantIDKey = @"tenantId"; + +/** + @var kIDTokenKey + @brief The key for idToken value in the request. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** + @var kAuthRegistrationRespKey + @brief The key for registration object from the authenticator. + */ +static NSString *const kAuthRegistrationRespKey = @"authenticatorRegistrationResponse"; + +/** + @var kNameKey + @brief The key of passkey name. + */ +static NSString *const kNameKey = @"name"; + +/** + @var kCredentialIDKey + @brief The key for registered credential identifier. + */ +static NSString *const kCredentialIDKey = @"credentialId"; + +/** + @var kAuthAttestationRespKey + @brief The key for attestation response from a FIDO authenticator. + */ +static NSString *const kAuthAttestationRespKey = @"authenticatorAttestationResponse"; + +/** + @var kClientDataJsonKey + @brief The key for CollectedClientData object from the authenticator. + */ +static NSString *const kClientDataJsonKey = @"clientDataJson"; + +/** + @var kAttestationObject + @brief The key for the attestation object from the authenticator. + */ +static NSString *const kAttestationObject = @"attestationObject"; + + +@implementation FIRFinalizePasskeyEnrollmentRequest + +- (nullable instancetype)initWithIDToken:(NSString *)IDToken + name:(NSString *)name + credentialID:(NSString *)credentialID + clientDataJson:(NSString *)clientDataJson + attestationObject:(NSString *)attestationObject requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kFinalizePasskeyEnrollmentEndPoint + requestConfiguration:requestConfiguration]; + if (self) { + self.useIdentityPlatform = YES; + _IDToken = IDToken; + _name = name; + _credentialID = credentialID; + _clientDataJson = clientDataJson; + _attestationObject = attestationObject; + } + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error { + NSMutableDictionary *postBody = [NSMutableDictionary dictionary]; + NSMutableDictionary *authRegistrationResponse = [NSMutableDictionary dictionary]; + NSMutableDictionary *authAttestationResponse = [NSMutableDictionary dictionary]; + + if (_IDToken) { + postBody[kIDTokenKey] = _IDToken; + } + if (_name) { + postBody[kNameKey] = _name; + } + if (_credentialID) { + authRegistrationResponse[kCredentialIDKey] = _credentialID; + } + if (_clientDataJson) { + authAttestationResponse[kClientDataJsonKey] = _clientDataJson; + } + if (_attestationObject) { + authAttestationResponse[kAttestationObject] = _attestationObject; + } + if (self.tenantID) { + postBody[kTenantIDKey] = self.tenantID; + } + + authRegistrationResponse[kAuthAttestationRespKey] = authAttestationResponse; + postBody[kAuthRegistrationRespKey] = authRegistrationResponse; + + return [postBody copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h new file mode 100644 index 000000000000..21c553bf9a8c --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h @@ -0,0 +1,41 @@ +/* + * 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 FIRFinalizePasskeyEnrollmentResponse + @brief Represents the response from the startPasskeyEnrollment endpoint. + */ +@interface FIRFinalizePasskeyEnrollmentResponse : NSObject + +/** + @property idToken + @brief The user raw access token. + */ +@property(nonatomic, readonly, copy) NSString *idToken; + +/** + @property refershToken + @brief Refresh token for the authenticated user. + */ +@property(nonatomic, copy, readonly) NSData *refreshToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.m new file mode 100644 index 000000000000..603560c99308 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.m @@ -0,0 +1,48 @@ +/* + * 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/FIRFinalizePasskeyEnrollmentResponse.h" + +/** + @var kIDTokenKey + @brief The name of the field in the response JSON for id token. + */ +static const NSString *kIdTokenKey = @"idToken"; + +/** + @var kRefreshTokenKey + @brief The name of the field in the response JSON for refresh token. + */ +static const NSString *kRefreshTokenKey = @"refreshToken"; + + +@implementation FIRFinalizePasskeyEnrollmentResponse + +- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary + error:(NSError *__autoreleasing _Nullable *_Nullable)error { + if (dictionary[kIdTokenKey] == nil) { + return NO; + } + if (dictionary[kRefreshTokenKey] == nil) { + return NO; + } + + _idToken = dictionary[kIdTokenKey]; + _refreshToken = dictionary[kRefreshTokenKey]; + return YES; +} + +@end diff --git a/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m new file mode 100644 index 000000000000..cfe984ab53e9 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m @@ -0,0 +1,172 @@ +/* + * 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/FIRFinalizePasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.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:finalize?key=APIKey"; + +/** + @var kIDToken + @brief Token representing the user's identity. + */ +static NSString *const kIDToken = @"testIDToken"; + +/** + @var kIDTokenKey + @brief ID Token field. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** + @var kName + @brief Passkey name. + */ +static NSString *const kName = @"testName"; + +/** + @var kNameKey + @brief Passkey name field + */ +static NSString *const kNameKey = @"name"; + +/** + @var kCredentialID + @brief credential ID. + */ +static NSString *const kCredentialID = @"testCredentialID"; + +/** + @var kCredentialIDKey + @brief credential ID field. + */ +static NSString *const kCredentialIDKey = @"credentialId"; + +/** + @var kRawAttestationObject + @brief Passkey attestation object. + */ +static NSString *const kRawAttestationObject = @"testRawAttestationObject"; + +/** + @var kRawAttestationObjectKey + @brief The key for the attestation object from the authenticator. + */ +static NSString *const kRawAttestationObjectKey = @"attestationObject"; + +/** + @var kRawClientDataJSON + @brief CollectedClientData object from the authenticator. + */ +static NSString *const kRawClientDataJSON = @"testRawClientDataJSON"; + +/** + @var kRawClientDataJSONKey + @brief The key for the attestation object from the authenticator. + */ +static NSString *const kRawClientDataJSONKey = @"clientDataJson"; + +/** + @var kAuthRegistrationRespKey + @brief The registration object from the authenticator. + */ +static NSString *const kAuthRegistrationRespKey = @"authenticatorRegistrationResponse"; + +/** + @var kAuthAttestationRespKey + @brief The key for attestation response from a FIDO authenticator. + */ +static NSString *const kAuthAttestationRespKey = @"authenticatorAttestationResponse"; + +/** + @class FIRFinalizePasskeyEnrollmentRequestTests + @brief Tests for @c FIRFinalizePasskeyEnrollmentRequest. + */ +@interface FIRFinalizePasskeyEnrollmentRequestTests : XCTestCase +@end + +@implementation FIRFinalizePasskeyEnrollmentRequestTests { + /** + @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)testFinalizePasskeyEnrollmentRequest { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeyEnrollmentRequest *request = [[FIRFinalizePasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken name:kName credentialID:kCredentialID clientDataJson:kRawClientDataJSON attestationObject:kRawAttestationObject requestConfiguration:_requestConfiguration]; + + [FIRAuthBackend finalizePasskeyEnrollment:request callback:^(FIRFinalizePasskeyEnrollmentResponse * _Nullable response, NSError * _Nullable error) { + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kIDToken); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kNameKey], kName); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthRegistrationRespKey][kAuthAttestationRespKey][kRawClientDataJSONKey], kRawClientDataJSON); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthRegistrationRespKey][kAuthAttestationRespKey][kRawAttestationObjectKey], kRawAttestationObject); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthRegistrationRespKey][kCredentialIDKey], kCredentialID); + + } +} + +@end +#endif diff --git a/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentResponseTests.m b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentResponseTests.m new file mode 100644 index 000000000000..0b461da39278 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentResponseTests.m @@ -0,0 +1,221 @@ +/* + * 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/FIRFinalizePasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.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 kRefreshToken + @brief Refresh Token + */ +static NSString *const kRefreshToken = @"refreshToken"; + +/** + @var kName + @brief Passkey name. + */ +static NSString *const kName = @"testName"; + +/** + @var kCredentialID + @brief credential ID. + */ +static NSString *const kCredentialID = @"testCredentialID"; + +/** + @var kRawAttestationObject + @brief Passkey attestation object. + */ +static NSString *const kRawAttestationObject = @"testRawAttestationObject"; + +/** + @var kRawClientDataJSON + @brief Passkey client data json. + */ +static NSString *const kRawClientDataJSON = @"testRawClientDataJSON"; + +/** + @class FIRFinalizePasskeyEnrollmentResponseTests + @brief Tests for @c FIRFinalizePasskeyEnrollmentResponse. + */ +@interface FIRFinalizePasskeyEnrollmentResponseTests : XCTestCase +@end +@implementation FIRFinalizePasskeyEnrollmentResponseTests { + /** + @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 testSuccessfulFinalizePasskeyEnrollmentResponse + @brief This test simulates a successful @c FinalizePasskeyEnrollment flow. + */ +- (void)testSuccessfulFinalizePasskeyEnrollmentResponse { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeyEnrollmentRequest *request = [[FIRFinalizePasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken name:kName credentialID:kCredentialID clientDataJson:kRawClientDataJSON attestationObject:kRawAttestationObject requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeyEnrollment:request + callback:^(FIRFinalizePasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"idToken" : kIDToken, + @"refreshToken": kRefreshToken, + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.idToken, kIDToken); + XCTAssertEqualObjects(RPCResponse.refreshToken, kRefreshToken); + } +} + +/** @fn testFinalizePasskeyEnrollmentResponseMissingIDTokenError + @brief This test simulates an unexpected response returned from server in @c + FinalizePasskeyEnrollment flow. + */ +- (void)testFinalizePasskeyEnrollmentResponseMissingIDTokenError { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeyEnrollmentRequest *request = [[FIRFinalizePasskeyEnrollmentRequest alloc] initWithIDToken:kIDToken name:kName credentialID:kCredentialID clientDataJson:kRawClientDataJSON attestationObject:kRawAttestationObject requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeyEnrollment:request + callback:^(FIRFinalizePasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"wrongkey" : @{}, + @"refreshToken" : kRefreshToken, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; + } +} +/** @fn testFinalizePasskeyEnrollmentResponseMissingRefreshTokenError + @brief This test simulates an unexpected response returned from server in @c + FinalizePasskeyEnrollment flow. + */ +- (void)testFinalizePasskeyEnrollmentResponseMissingRefreshTokenError { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeyEnrollmentRequest *request = [[FIRFinalizePasskeyEnrollmentRequest alloc]initWithIDToken:kIDToken name:kName credentialID:kCredentialID clientDataJson:kRawClientDataJSON attestationObject:kRawAttestationObject requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeyEnrollmentResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeyEnrollment:request + callback:^(FIRFinalizePasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"wrongkey" : @{}, + @"idToken" : kIDToken, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; + } +} + +/** @fn errorValidationHelperWithCallbackInvoked:rpcError:rpcResponse: + @brief Helper function to validate the unexpected response returned from server in @c + FinalizePasskeyEnrollment flow. + */ +- (void)errorValidationHelperWithCallbackInvoked:(BOOL)callbackInvoked + rpcError:(NSError *)RPCError + rpcResponse:(FIRFinalizePasskeyEnrollmentResponse *)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 +