diff --git a/FirebaseAuth/Sources/Auth/FIRAuth.m b/FirebaseAuth/Sources/Auth/FIRAuth.m index d1c84c173e8..bce46c7f275 100644 --- a/FirebaseAuth/Sources/Auth/FIRAuth.m +++ b/FirebaseAuth/Sources/Auth/FIRAuth.m @@ -40,6 +40,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRCreateAuthURIResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRResetPasswordRequest.h" @@ -54,6 +56,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/FIRStartPasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyCustomTokenRequest.h" @@ -82,6 +86,10 @@ #import "FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.h" #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#import +#endif + NS_ASSUME_NONNULL_BEGIN #pragma mark-- Logger Service String. @@ -1216,6 +1224,88 @@ - (void)signInWithCustomToken:(NSString *)token }); } +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +- (void)startPasskeySignInWithCompletion: + (nullable void (^)( + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *_Nullable request, + NSError *_Nullable error))completion { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:self.requestConfiguration]; + [FIRAuthBackend + startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + if (response) { + NSData *challengeInData = + [[NSData alloc] initWithBase64EncodedString:response.challenge options:0]; + ASAuthorizationPlatformPublicKeyCredentialProvider *provider = + [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:response.rpID]; + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *request = + [provider createCredentialAssertionRequestWithChallenge:challengeInData]; + + completion(request, nil); + } + }]; +} + +- (void)finalizePasskeySignInWithPlatformCredential: + (ASAuthorizationPlatformPublicKeyCredentialAssertion *)platformCredential + completion:(nullable void (^)( + FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error))completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRAuthDataResultCallback decoratedCallback = + [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; + NSString *credentialID = [platformCredential.credentialID base64EncodedStringWithOptions:0]; + NSString *clientDataJson = + [platformCredential.rawClientDataJSON base64EncodedStringWithOptions:0]; + NSString *authenticatorData = + [platformCredential.rawAuthenticatorData base64EncodedStringWithOptions:0]; + NSString *signature = [platformCredential.signature base64EncodedStringWithOptions:0]; + NSString *userID = [platformCredential.userID base64EncodedStringWithOptions:0]; + FIRFinalizePasskeySignInRequest *request = + [[FIRFinalizePasskeySignInRequest alloc] initWithCredentialID:credentialID + clientDataJson:clientDataJson + authenticatorData:authenticatorData + signature:signature + userID:userID + requestConfiguration:self.requestConfiguration]; + [FIRAuthBackend + finalizePasskeySignIn:request + callback:^(FIRFinalizePasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + decoratedCallback(nil, error); + return; + } + [self completeSignInWithAccessToken:response.idToken + accessTokenExpirationDate:nil + refreshToken:response.refreshToken + anonymous:NO + callback:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + + FIRAuthDataResult *authDataResult = + user ? [[FIRAuthDataResult alloc] + initWithUser:user + additionalUserInfo:nil] + : nil; + decoratedCallback(authDataResult, error); + }]; + }]; + }); +} +#endif // #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST + - (void)createUserWithEmail:(NSString *)email password:(NSString *)password completion:(nullable FIRAuthDataResultCallback)completion { diff --git a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h index 997fdc91def..532f9a9df9c 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.h +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.h @@ -58,6 +58,14 @@ @class FIRRevokeTokenResponse; @class FIRGetRecaptchaConfigRequest; @class FIRGetRecaptchaConfigResponse; +@class FIRStartPasskeyEnrollmentRequest; +@class FIRStartPasskeyEnrollmentResponse; +@class FIRFinalizePasskeyEnrollmentRequest; +@class FIRFinalizePasskeyEnrollmentResponse; +@class FIRStartPasskeySignInRequest; +@class FIRStartPasskeySignInResponse; +@class FIRFinalizePasskeySignInRequest; +@class FIRFinalizePasskeySignInResponse; @protocol FIRAuthBackendImplementation; @protocol FIRAuthBackendRPCIssuer; @@ -243,15 +251,60 @@ 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); + +/** + @typedef FIRFinalizePasskeyEnrollmentResponseCallback + @brief The type of block used to return the result of a call to the finalizePasskeyEnrollment + 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); + +/** + @typedef FIRStartPasskeySignInResponseCallback + @brief The type of block used to return the result of a call to the StartPasskeySignIn +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 (^FIRStartPasskeySignInResponseCallback)( + FIRStartPasskeySignInResponse *_Nullable response, NSError *_Nullable error); + +/** + @typedef FIRFinalizePasskeySignInResponseCallback + @brief The type of block used to return the result of a call to the finalizePasskeySignIn + 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 (^FIRFinalizePasskeySignInResponseCallback)( + FIRFinalizePasskeySignInResponse *_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 +501,42 @@ 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; + +/** @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; + +/** @fn startPasskeySignIn:callback: + @brief Calls the startPasskeySignIn endpoint, which is responsible for receving the + challenge that will later be consumed for platform key attestation. + @param request The request parameters. + @param callback The callback. + */ ++ (void)startPasskeySignIn:(FIRStartPasskeySignInRequest *)request + callback:(FIRStartPasskeySignInResponseCallback)callback; + +/** @fn finalizePasskeySignIn:callback: + @brief Sends the platform created public info to the finalizePasskeySignIn endpoint. + @param request The request parameters. + @param callback The callback. + */ ++ (void)finalizePasskeySignIn:(FIRFinalizePasskeySignInRequest *)request + callback:(FIRFinalizePasskeySignInResponseCallback)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 +711,42 @@ 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; + +/** @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; + +/** @fn startPasskeySignIn:callback: + @brief Calls the startPasskeySignIn endpoint, which is responsible for receving the challange. + @param request The request parameters. + @param callback The callback. + */ +- (void)startPasskeySignIn:(FIRStartPasskeySignInRequest *)request + callback:(FIRStartPasskeySignInResponseCallback)callback; + +/** @fn finalizePasskeySignIn:callback: + @brief Sends the platform created public info to the finalizePasskeySignIn endpoint. + @param request The request parameters. + @param callback The callback. + */ +- (void)finalizePasskeySignIn:(FIRFinalizePasskeySignInRequest *)request + callback:(FIRFinalizePasskeySignInResponseCallback)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 0ee39aba877..ea1d2ab8112 100644 --- a/FirebaseAuth/Sources/Backend/FIRAuthBackend.m +++ b/FirebaseAuth/Sources/Backend/FIRAuthBackend.m @@ -36,6 +36,10 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRDeleteAccountResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeRequest.h" @@ -58,6 +62,10 @@ #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/FIRStartPasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyClientRequest.h" @@ -553,10 +561,16 @@ /** @var kInvalidLoginCredentials @brief This is the error message the server will respond with if the login credentials is - invalid. in the request. + invalid in the request. */ static NSString *const kInvalidLoginCredentials = @"INVALID_LOGIN_CREDENTIALS"; +/** @var kPasskeyEnrollmentNotFound + @brief This is the error message the server will respond with if the passkey credentials is + invalid in the request. + */ +static NSString *const kPasskeyEnrollmentNotFound = @"PASSKEY_ENROLLMENT_NOT_FOUND"; + /** @var gBackendImplementation @brief The singleton FIRAuthBackendImplementation instance to use. */ @@ -679,6 +693,28 @@ + (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callb #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST ++ (void)startPasskeySignIn:(FIRStartPasskeySignInRequest *)request + callback:(FIRStartPasskeySignInResponseCallback)callback { + [[self implementation] startPasskeySignIn:request callback:callback]; +} + ++ (void)finalizePasskeySignIn:(FIRFinalizePasskeySignInRequest *)request + callback:(FIRFinalizePasskeySignInResponseCallback)callback { + [[self implementation] finalizePasskeySignIn:request callback:callback]; +} + ++ (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 callback:(FIRRevokeTokenResponseCallback)callback { [[self implementation] revokeToken:request callback:callback]; @@ -1105,6 +1141,67 @@ - (void)verifyClient:(id)request callback:(FIRVerifyClientResponseCallback)callb #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST + +- (void)startPasskeySignIn:(FIRStartPasskeySignInRequest *)request + callback:(FIRStartPasskeySignInResponseCallback)callback { + FIRStartPasskeySignInResponse *response = [[FIRStartPasskeySignInResponse alloc] init]; + [self callWithRequest:request + response:response + callback:^(NSError *error) { + if (error) { + callback(nil, error); + return; + } + callback(response, nil); + }]; +} + +- (void)finalizePasskeySignIn:(FIRFinalizePasskeySignInRequest *)request + callback:(FIRFinalizePasskeySignInResponseCallback)callback { + FIRFinalizePasskeySignInResponse *response = [[FIRFinalizePasskeySignInResponse alloc] init]; + [self callWithRequest:request + response:response + callback:^(NSError *error) { + if (error) { + callback(nil, error); + return; + } + callback(response, nil); + }]; +} + +- (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); + }]; +} + +- (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 callback:(FIRRevokeTokenResponseCallback)callback { FIRRevokeTokenResponse *response = [[FIRRevokeTokenResponse alloc] init]; @@ -1309,7 +1406,6 @@ - (void)callWithRequest:(id)request underlyingError:error]); return; } - // Finally, we try to populate the response object with the JSON // values. if (![response setWithDictionary:dictionary error:&error]) { @@ -1384,6 +1480,10 @@ + (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorM return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage]; } + if ([shortErrorMessage isEqualToString:kPasskeyEnrollmentNotFound]) { + return [FIRAuthErrorUtils passkeyEnrollmentNotFoundError]; + } + if ([shortErrorMessage isEqualToString:kUserDeletedErrorMessage]) { return [FIRAuthErrorUtils userNotFoundErrorWithMessage:serverDetailErrorMessage]; } diff --git a/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h b/FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h index 0fe981a6eae..ee1a71fa182 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 00000000000..53836f50048 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h @@ -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/FIRAuthRPCRequest.h" +#import "FirebaseAuth/Sources/Backend/FIRIdentityToolkitRequest.h" + +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 00000000000..1e610919882 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.m @@ -0,0 +1,128 @@ +/* + * 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 = @"id"; + +/** + @var kAuthAttestationRespKey + @brief The key for attestation response from a FIDO authenticator. + */ +static NSString *const kAuthAttestationRespKey = @"response"; + +/** + @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; + self.useStaging = NO; + _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 00000000000..826ba402ad6 --- /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) NSString *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 00000000000..93f4334871b --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.m @@ -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/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/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h new file mode 100644 index 00000000000..778d3328970 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h @@ -0,0 +1,66 @@ +/* + * 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 FIRFinalizePasskeySignInRequest + @brief Represents the parameters for the finalizePasskeySignIn endpoint. + */ +@interface FIRFinalizePasskeySignInRequest : FIRIdentityToolkitRequest + +/** + @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 authenticatorData + @brief The AuthenticatorData from the authenticator. + */ +@property(nonatomic, copy, readonly) NSString *authenticatorData; + +/** + @property signature + @brief The signature from the authenticator. + */ +@property(nonatomic, copy, readonly) NSString *signature; + +/** + @property userID + @brief The user handle + */ +@property(nonatomic, copy, readonly) NSString *userID; + +- (nullable instancetype)initWithCredentialID:(NSString *)credentialID + clientDataJson:(NSString *)clientDataJson + authenticatorData:(NSString *)authenticatorData + signature:(NSString *)signature + userID:(NSString *)userID + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.m new file mode 100644 index 00000000000..9ca41279285 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.m @@ -0,0 +1,133 @@ +/* + * 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/FIRFinalizePasskeySignInRequest.h" +NS_ASSUME_NONNULL_BEGIN + +/** + @var kFinalizePasskeySignInEndPoint + @brief GCIP endpoint for finalizePasskeySignIn rpc + */ +static NSString *const kFinalizePasskeySignInEndPoint = @"accounts/passkeySignIn:finalize"; + +/** + @var kTenantIDKey + @brief The key for the tenant id value in the request. + */ +static NSString *const kTenantIDKey = @"tenantId"; + +/** + @var kAuthenticatorAuthRespKey + @brief The key for authentication response object from the authenticator. + */ +static NSString *const kAuthenticatorAuthRespKey = @"authenticatorAuthenticationResponse"; + +/** + @var kCredentialIDKey + @brief The key for registered credential identifier. + */ +static NSString *const kCredentialIDKey = @"id"; + +/** + @var kAuthAssertionRespKey + @brief The key for authentication assertion from the authenticator. + */ +static NSString *const kAuthAssertionRespKey = @"response"; + +/** + @var kClientDataJsonKey + @brief The key for CollectedClientData object from the authenticator. + */ +static NSString *const kClientDataJsonKey = @"clientDataJSON"; + +/** + @var kAuthenticatorDataKey + @brief The key for authenticatorData from the authenticator. + */ +static NSString *const kAuthenticatorDataKey = @"authenticatorData"; + +/** + @var kSignatureKey + @brief The key for the signature from the authenticator. + */ +static NSString *const kSignatureKey = @"signature"; + +/** + @var kUserHandleKey + @brief The key for the user handle. This is the same as user ID. + */ +static NSString *const kUserHandleKey = @"userHandle"; + +@implementation FIRFinalizePasskeySignInRequest + +- (nullable instancetype)initWithCredentialID:(NSString *)credentialID + clientDataJson:(NSString *)clientDataJson + authenticatorData:(NSString *)authenticatorData + signature:(NSString *)signature + userID:(NSString *)userID + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kFinalizePasskeySignInEndPoint + requestConfiguration:requestConfiguration]; + if (self) { + self.useIdentityPlatform = YES; + self.useStaging = NO; + _credentialID = credentialID; + _clientDataJson = clientDataJson; + _authenticatorData = authenticatorData; + _signature = signature; + _userID = userID; + } + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error { + NSMutableDictionary *postBody = [NSMutableDictionary dictionary]; + NSMutableDictionary *authenticatorAuthResponse = [NSMutableDictionary dictionary]; + NSMutableDictionary *authAssertionResponse = [NSMutableDictionary dictionary]; + + if (self.tenantID) { + postBody[kTenantIDKey] = self.tenantID; + } + + if (_credentialID) { + authenticatorAuthResponse[kCredentialIDKey] = _credentialID; + } + + if (_clientDataJson) { + authAssertionResponse[kClientDataJsonKey] = _clientDataJson; + } + + if (_authenticatorData) { + authAssertionResponse[kAuthenticatorDataKey] = _authenticatorData; + } + + if (_signature) { + authAssertionResponse[kSignatureKey] = _signature; + } + + if (_userID) { + authAssertionResponse[kUserHandleKey] = _userID; + } + + authenticatorAuthResponse[kAuthAssertionRespKey] = authAssertionResponse; + postBody[kAuthenticatorAuthRespKey] = authenticatorAuthResponse; + + return [postBody copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.h new file mode 100644 index 00000000000..ba4b8489b73 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.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 FIRFinalizePasskeySignInResponse + @brief Represents the response from the finalizePasskeySignIn endpoint. + */ +@interface FIRFinalizePasskeySignInResponse : 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) NSString *refreshToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.m new file mode 100644 index 00000000000..e409c4e4ff3 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.m @@ -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/RPC/FIRFinalizePasskeySignInResponse.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 FIRFinalizePasskeySignInResponse + +- (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/Sources/Backend/RPC/FIRGetAccountInfoResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h index 14e7f88c41a..ef4f3618c15 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h +++ b/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h @@ -16,6 +16,7 @@ #import "FirebaseAuth/Sources/Backend/FIRAuthRPCResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/Proto/FIRAuthProtoMFAEnrollment.h" +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h" NS_ASSUME_NONNULL_BEGIN @@ -129,6 +130,12 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, readonly, nullable) NSArray *MFAEnrollments; +/** + @property enrolledPasskeys + @brief A list of enrolled passkeys of the user. + */ +@property(nonatomic, strong, readonly, nullable) NSArray *enrolledPasskeys; + /** @fn init @brief Please use initWithDictionary: */ diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.m index 6b812bd9097..ab5de61dc68 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.m +++ b/FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.m @@ -15,6 +15,8 @@ */ #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo_Internal.h" +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h" #import "FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h" @@ -90,6 +92,16 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary { } _MFAEnrollments = [MFAEnrollments copy]; } + // Get enrolled passkey list + NSArray *passkeyEnrollmentData = dictionary[@"passkeyInfo"]; + if (passkeyEnrollmentData) { + NSMutableArray *enrolledPasskeys = + [NSMutableArray arrayWithCapacity:passkeyEnrollmentData.count]; + for (NSDictionary *dictionary in passkeyEnrollmentData) { + [enrolledPasskeys addObject:[[FIRPasskeyInfo alloc] initWithDictionary:dictionary]]; + } + _enrolledPasskeys = [enrolledPasskeys copy]; + } } return self; } diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h index 5ba22baed6e..3124fbbe7c7 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h +++ b/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.h @@ -126,6 +126,11 @@ extern NSString *const FIRSetAccountInfoUserAttributePassword; */ @property(nonatomic, copy, nullable) NSArray *deleteProviders; +/** @property deletePasskeys + @brief The list of credential IDs of the passkeys to delete. + */ +@property(nonatomic, copy, nullable) NSArray *deletePasskeys; + /** @property returnSecureToken @brief Whether the response should return access token and refresh token directly. @remarks The default value is @c YES . diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.m index 6e7291e61c6..e0ca1535c94 100644 --- a/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.m +++ b/FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoRequest.m @@ -108,6 +108,11 @@ */ static NSString *const kDeleteProvidersKey = @"deleteProvider"; +/** @var kDeletePasskeysKey + @brief The key for the "deletePasskey" value in the request. + */ +static NSString *const kDeletePasskeysKey = @"deletePasskey"; + /** @var kReturnSecureTokenKey @brief The key for the "returnSecureToken" value in the request. */ @@ -170,6 +175,9 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) if (_deleteAttributes) { postBody[kDeleteAttributesKey] = _deleteAttributes; } + if (_deletePasskeys) { + postBody[kDeletePasskeysKey] = _deletePasskeys; + } if (_deleteProviders) { postBody[kDeleteProvidersKey] = _deleteProviders; } 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..35877750426 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeyEnrollmentRequest.m @@ -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/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; + self.useStaging = NO; + } + + 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/Sources/Backend/RPC/FIRStartPasskeySignInRequest.h b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.h new file mode 100644 index 00000000000..bdbfaa5647f --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.h @@ -0,0 +1,32 @@ +/* + * 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 FIRStartPasskeySignInRequest + @brief Represents the parameters for the startPasskeySignIn endpoint. + */ +@interface FIRStartPasskeySignInRequest : FIRIdentityToolkitRequest + +- (nullable instancetype)initWithRequestConfiguration: + (FIRAuthRequestConfiguration *)requestConfiguration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.m b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.m new file mode 100644 index 00000000000..7b8bb91fbfb --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.m @@ -0,0 +1,58 @@ +/* + * 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/FIRStartPasskeySignInRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + @var kStartPasskeySignInEndPoint + @brief GCIP endpoint for startPasskeySignIn rpc + */ +static NSString *const kStartPasskeySignInEndPoint = @"accounts/passkeySignIn:start"; + +/** + @var kTenantIDKey + @brief The key for the tenant id value in the request. + */ +static NSString *const kTenantIDKey = @"tenantId"; + +@implementation FIRStartPasskeySignInRequest + +- (nullable instancetype)initWithRequestConfiguration: + (FIRAuthRequestConfiguration *)requestConfiguration { + self = [super initWithEndpoint:kStartPasskeySignInEndPoint + requestConfiguration:requestConfiguration]; + + if (self) { + self.useIdentityPlatform = YES; + self.useStaging = NO; + } + + return self; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing _Nullable *)error { + NSMutableDictionary *postBody = [NSMutableDictionary dictionary]; + if (self.tenantID) { + postBody[kTenantIDKey] = self.tenantID; + } + return [postBody copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.h b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.h new file mode 100644 index 00000000000..128562ce3c6 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.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 FIRStartPasskeySignInResponse + @brief Represents the response from the startPasskeySignIn endpoint. + */ +@interface FIRStartPasskeySignInResponse : NSObject + +/** + @property rpID + @brief The RP ID of the FIDO Relying Party. + */ +@property(nonatomic, readonly, copy) NSString *rpID; + +/** + @property challenge + @brief The FIDO challenge. + */ +@property(nonatomic, readonly, copy) NSString *challenge; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.m b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.m new file mode 100644 index 00000000000..6729c4d5573 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.m @@ -0,0 +1,56 @@ +/* + * 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/FIRStartPasskeySignInResponse.h" + +/** + @var kOptionsKey + @brief Parameters specified for the authenticator to sign a challenge. + */ +static const NSString *kOptionsKey = @"credentialRequestOptions"; + +/** + @var kRpIdKey + @brief The relying party identifier. + */ +static const NSString *kRpIdKey = @"rpId"; + +/** + @var kChallengeKey + @brief The name of the field in the response JSON for challenge. + */ +static const NSString *kChallengeKey = @"challenge"; + +@implementation FIRStartPasskeySignInResponse + +- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary + error:(NSError *__autoreleasing _Nullable *_Nullable)error { + if (dictionary[kOptionsKey] == nil) { + return NO; + } + if (dictionary[kOptionsKey][kRpIdKey] == nil) { + return NO; + } + if (dictionary[kOptionsKey][kChallengeKey] == nil) { + return NO; + } + + _rpID = dictionary[kOptionsKey][kRpIdKey]; + _challenge = dictionary[kOptionsKey][kChallengeKey]; + return YES; +} + +@end diff --git a/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo.m b/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo.m new file mode 100644 index 00000000000..69cf1658d06 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo.m @@ -0,0 +1,50 @@ +/* + * 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/Public/FirebaseAuth/FIRPasskeyInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + @var kNameKey + @brief The name of the field in the response JSON for name. + */ +static const NSString *kNameKey = @"name"; + +/** + @var kCredentialIdKey + @brief The name of the field in the response JSON for credential ID. + */ +static const NSString *kCredentialIdKey = @"credentialId"; + +@implementation FIRPasskeyInfo + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [super init]; + if (self) { + if (dictionary[kNameKey]) { + _name = dictionary[kNameKey]; + } + if (dictionary[kCredentialIdKey]) { + _credentialID = dictionary[kCredentialIdKey]; + } + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo_Internal.h b/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo_Internal.h new file mode 100644 index 00000000000..c5dd00c9287 --- /dev/null +++ b/FirebaseAuth/Sources/Backend/RPC/Proto/FIRPasskeyInfo_Internal.h @@ -0,0 +1,26 @@ +/* + * 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/Proto/FIRAuthProto.h" +#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRPasskeyInfo () + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h index 3395a986a7d..fca4fd85ee8 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuth.h @@ -27,6 +27,12 @@ @class FIRAuthDataResult; @class FIRAuthSettings; @class FIRUser; + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +@class ASAuthorizationPlatformPublicKeyCredentialAssertion; +@class ASAuthorizationPlatformPublicKeyCredentialAssertionRequest; +#endif + @protocol FIRAuthUIDelegate; @protocol FIRFederatedAuthProvider; @@ -586,6 +592,40 @@ NS_SWIFT_NAME(Auth) completion:(nullable void (^)(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error))completion; +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** + @fn startPasskeySignInWithCompletion: + @brief start sign in with passkey retrieving challenge from GCIP and create an assertion request. + @param completion Optionally; a block which creates a assertation request. + + @remarks // TODO @liubinj add possible error codes + + */ +- (void)startPasskeySignInWithCompletion: + (nullable void (^)( + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *_Nullable request, + NSError *_Nullable error))completion NS_SWIFT_NAME(startPasskeySignIn(completion:)) + API_AVAILABLE(macos(12.0), ios(15.0), tvos(16.0)); + +/** + @fn finalizePasskeySignInWithPlatformCredential:completion: + @brief finalize sign in with passkey with existing credential assertion. + @param platformCredential The existing credential assertion created by device. + @param completion Optionally; a block which is invoked when the sign in with passkey flow finishes, + or is canceled. Invoked asynchronously on the main thread in the future. + + @remarks // TODO @liubinj add possible error codes + + */ +- (void)finalizePasskeySignInWithPlatformCredential: + (ASAuthorizationPlatformPublicKeyCredentialAssertion *)platformCredential + completion:(nullable void (^)( + FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error))completion + NS_SWIFT_NAME(finalizePasskeySignIn(with:completion:)) + API_AVAILABLE(macos(12.0), ios(15.0), tvos(16.0)); +#endif + /** @fn createUserWithEmail:password:completion: @brief Creates and, on success, signs in a user with the given email address and password. diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h index d6bfa44979f..59badcfac5b 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthErrors.h @@ -458,6 +458,10 @@ typedef NS_ERROR_ENUM(FIRAuthErrorDomain, FIRAuthErrorCode){ */ FIRAuthErrorCodeRecaptchaSDKNotLinked = 17208, + /** Indicates the user account was not found. + */ + FIRAuthErrorCodePasskeyEnrollmentNotFound = 17209, + /** Indicates an error occurred while attempting to access the keychain. */ FIRAuthErrorCodeKeychainError = 17995, diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h new file mode 100644 index 00000000000..97cd2c37f5d --- /dev/null +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRPasskeyInfo.h @@ -0,0 +1,39 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + @class FIRPasskeyInfo + @brief Passkey Info + */ +NS_SWIFT_NAME(PasskeyInfo) @interface FIRPasskeyInfo : NSObject + +/** + @brief Passkey name + */ +@property(nonatomic, readonly) NSString *name; + +/** + @brief Passkey credential ID + */ +@property(nonatomic, readonly) NSString *credentialID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRUser.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRUser.h index b4cb8b041d0..c9efc0955fd 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRUser.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRUser.h @@ -19,6 +19,7 @@ #import "FIRAuth.h" #import "FIRAuthDataResult.h" #import "FIRMultiFactor.h" +#import "FIRPasskeyInfo.h" #import "FIRUserInfo.h" @class FIRAuthTokenResult; @@ -26,6 +27,10 @@ @class FIRUserProfileChangeRequest; @class FIRUserMetadata; @protocol FIRAuthUIDelegate; +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +@class ASAuthorizationPlatformPublicKeyCredentialRegistration; +@class ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest; +#endif NS_ASSUME_NONNULL_BEGIN @@ -123,6 +128,11 @@ NS_SWIFT_NAME(User) @property(nonatomic, readonly, nonnull) FIRMultiFactor *multiFactor API_UNAVAILABLE(macos, tvos, watchos); +/** @property enrolledPasskeys + @brief a list of user enrolled passkey object. +*/ +@property(nonatomic, readonly) NSArray *enrolledPasskeys API_UNAVAILABLE(watchos); + /** @fn init @brief This class should not be instantiated. @remarks To retrieve the current user, use `Auth.currentUser`. To sign a user @@ -166,6 +176,55 @@ NS_SWIFT_NAME(User) "This method is deprecated and will be removed in a future release. Use " "sendEmailVerification(beforeUpdatingEmail email: String) instead."); +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** + @fn startPasskeyEnrollmentWithName:completion: + @brief Start the passkey enrollment creating a plaform public key creation request with the + challenge from GCIP backend. + + @param name The name for the passkey to be created. + @param completion Optionally; the block which is invoked when start passkey enrollment flow + finishes. + + @remarks Possible error codes: // TODO @liubinj fill this after + */ +- (void)startPasskeyEnrollmentWithName:(nullable NSString *)name + completion: + (nullable void (^)( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError *_Nullable error))completion + NS_SWIFT_NAME(startPasskeyEnrollment(with:completion:)) + API_AVAILABLE(macos(12.0), ios(15.0), tvos(16.0)); + +/** + @fn finalizePasskeyEnrollmentWithPlatformCredential:completion: + @brief Finalize the passkey enrollment with the platfrom public key credential. + + @param platformCredential The name for the passkey to be created. + @param completion Optionally; a block which is invoked when the finalize enroll with passkey flow + finishes, or is canceled + + @remarks Possible error codes: // TODO @liubinj fill this after + */ +- (void)finalizePasskeyEnrollmentWithPlatformCredential: + (ASAuthorizationPlatformPublicKeyCredentialRegistration *)platformCredential + completion:(nullable void (^)( + FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error))completion + NS_SWIFT_NAME(finalizePasskeyEnrollment(with:completion:)) + API_AVAILABLE(macos(12.0), ios(15.0), tvos(16.0)); +#endif + +/** + @fn unenrollPasskeyWithCredentialID:completion + @brief To unenroll a passkey with platform credential. + @param credentialID the passkey credential ID to unenroll. + */ +- (void)unenrollPasskeyWithCredentialID:(NSString *)credentialID + completion:(nullable void (^)(NSError *_Nullable error))completion + NS_SWIFT_NAME(unenrollPasskey(with:completion:)); + /** @fn updatePassword:completion: @brief Updates the password for the user. On success, the cached user profile data is updated. diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h index 111ba066543..6d0a18e6d95 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h @@ -36,6 +36,7 @@ #import "FIRMultiFactorSession.h" #import "FIROAuthCredential.h" #import "FIROAuthProvider.h" +#import "FIRPasskeyInfo.h" #import "FIRTwitterAuthProvider.h" #import "FIRUser.h" #import "FIRUserInfo.h" diff --git a/FirebaseAuth/Sources/User/FIRUser.m b/FirebaseAuth/Sources/User/FIRUser.m index 24d4d7a376d..f1ae5c706dc 100644 --- a/FirebaseAuth/Sources/User/FIRUser.m +++ b/FirebaseAuth/Sources/User/FIRUser.m @@ -35,6 +35,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRDeleteAccountResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeyEnrollmentResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeRequest.h" @@ -45,6 +47,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/FIRVerifyCustomTokenRequest.h" @@ -68,6 +72,10 @@ #import "FirebaseAuth/Sources/Utilities/FIRAuthRecaptchaVerifier.h" #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#import +#endif + NS_ASSUME_NONNULL_BEGIN /** @var kUserIDCodingKey @@ -484,6 +492,7 @@ - (void)updateWithGetAccountInfoResponse:(FIRGetAccountInfoResponse *)response { _multiFactor = [[FIRMultiFactor alloc] initWithMFAEnrollments:user.MFAEnrollments]; _multiFactor.user = self; #endif + _enrolledPasskeys = [user.enrolledPasskeys copy]; } /** @fn executeUserUpdateWithChanges:callback: @@ -584,6 +593,155 @@ - (void)setTokenService:(FIRSecureTokenService *)tokenService } #pragma mark - + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +- (void)startPasskeyEnrollmentWithName:(nullable NSString *)name + completion: + (nullable void (^)( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError *_Nullable error))completion { + FIRAuthRequestConfiguration *requestConfiguration = self->_auth.requestConfiguration; + + FIRStartPasskeyEnrollmentRequest *request = + [[FIRStartPasskeyEnrollmentRequest alloc] initWithIDToken:self.rawAccessToken + requestConfiguration:requestConfiguration]; + [FIRAuthBackend + startPasskeyEnrollment:request + callback:^(FIRStartPasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + // reset the passkey name cache. + self.passkeyName = nil; + completion(nil, error); + return; + } else { + // cached the passkey name. This is needed when calling + // finalizePasskeyEnrollment + self.passkeyName = name; + // If passkey name is not provided, we will provide a firebase formatted + // default name. + if (self.passkeyName == nil || [self.passkeyName isEqual:@""]) { + self.passkeyName = @"Unnamed account (Apple)"; + } + NSData *challengeInData = + [[NSData alloc] initWithBase64EncodedString:response.challenge + options:0]; + NSData *userIdInData = + [[NSData alloc] initWithBase64EncodedString:response.userID options:0]; + + ASAuthorizationPlatformPublicKeyCredentialProvider *provider = + [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] + initWithRelyingPartyIdentifier:response.rpID]; + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest *request = + [provider + createCredentialRegistrationRequestWithChallenge:challengeInData + name:self.passkeyName + userID:userIdInData]; + completion(request, nil); + } + }]; +} + +- (void)finalizePasskeyEnrollmentWithPlatformCredential: + (ASAuthorizationPlatformPublicKeyCredentialRegistration *)platformCredential + completion:(nullable void (^)( + FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error))completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRAuthDataResultCallback decoratedCallback = + [FIRAuth.auth signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; + FIRAuthRequestConfiguration *requestConfiguration = self->_auth.requestConfiguration; + + NSString *credentialID = [platformCredential.credentialID base64EncodedStringWithOptions:0]; + NSString *clientDataJson = + [platformCredential.rawClientDataJSON base64EncodedStringWithOptions:0]; + NSString *attestationObject = + [platformCredential.rawAttestationObject base64EncodedStringWithOptions:0]; + + FIRFinalizePasskeyEnrollmentRequest *request = + [[FIRFinalizePasskeyEnrollmentRequest alloc] initWithIDToken:self.rawAccessToken + name:self.passkeyName + credentialID:credentialID + clientDataJson:clientDataJson + attestationObject:attestationObject + requestConfiguration:requestConfiguration]; + + [FIRAuthBackend + finalizePasskeyEnrollment:request + callback:^(FIRFinalizePasskeyEnrollmentResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + decoratedCallback(nil, error); + return; + } else { + [FIRAuth.auth + completeSignInWithAccessToken:response.idToken + accessTokenExpirationDate:nil + refreshToken:response.refreshToken + anonymous:NO + callback:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + + FIRAuthDataResult *authDataResult = + user ? [[FIRAuthDataResult alloc] + initWithUser:user + additionalUserInfo:nil] + : nil; + decoratedCallback(authDataResult, error); + }]; + } + }]; + }); +} +#endif // #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST + +- (void)unenrollPasskeyWithCredentialID:(NSString *)credentialID + completion:(nullable void (^)(NSError *_Nullable error))completion { + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] + initWithRequestConfiguration:self->_auth.requestConfiguration]; + request.deletePasskeys = @[ credentialID ]; + request.accessToken = self.rawAccessToken; + [FIRAuthBackend + setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, NSError *_Nullable error) { + if (error) { + callInMainThreadWithError(completion, error); + } else { + [FIRAuth.auth + completeSignInWithAccessToken:response.IDToken + accessTokenExpirationDate:response.approximateExpirationDate + refreshToken:response.refreshToken + anonymous:NO + callback:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + FIRAuthDataResult *result = + [[FIRAuthDataResult alloc] initWithUser:user + additionalUserInfo:nil]; + FIRAuthDataResultCallback decoratedCallback = [FIRAuth + .auth + signInFlowAuthDataResultCallbackByDecoratingCallback: + ^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + [[FIRAuth auth] signOut:NULL]; + } + if (completion) { + completion(error); + } + }]; + decoratedCallback(result, error); + }]; + } + }]; + }); +} + /** @fn updateEmail:password:callback: @brief Updates email address and/or password for the current user. @remarks May fail if there is already an email/password-based account for the same email diff --git a/FirebaseAuth/Sources/User/FIRUser_Internal.h b/FirebaseAuth/Sources/User/FIRUser_Internal.h index 54524f208c5..b3f158dfb7e 100644 --- a/FirebaseAuth/Sources/User/FIRUser_Internal.h +++ b/FirebaseAuth/Sources/User/FIRUser_Internal.h @@ -44,6 +44,12 @@ typedef void (^FIRVerifyBeforeUpdateEmailCallback)(NSError *_Nullable error); */ @property(nonatomic, copy, readonly) NSString *rawAccessToken; +/** + @property passkeyName + @brief A cached passkey name that being passed from startPasskeyEnrollmentWithName:completion: call + and consumed at finalizePasskeyEnrollmentWithPlatformCredential:completion: call + */ +@property(nonatomic, copy, nullable) NSString *passkeyName; /** @property auth @brief A weak reference to a FIRAuth instance associated with this instance. */ diff --git a/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h b/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h index 0e5e4b4c072..c384f2781eb 100644 --- a/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h +++ b/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h @@ -489,6 +489,12 @@ static NSString *const kMissingRecaptchaTokenErrorPrefix = @"MISSING_RECAPTCHA_T */ + (NSError *)notificationNotForwardedError; +/** @fn passkeyEnrollmentNotFoundError + @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNotificationNotForwarded code. + @return The NSError instance associated with the given FIRAuthError. + */ ++ (NSError *)passkeyEnrollmentNotFoundError; + #if TARGET_OS_IOS /** @fn secondFactorRequiredError @brief Constructs an @c NSError with the @c FIRAuthErrorCodeSecondFactorRequired code. diff --git a/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.m b/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.m index 5eb531b749d..42b6cc4a9e3 100644 --- a/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.m +++ b/FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.m @@ -630,6 +630,9 @@ @"Login credentials invalid. It is possible that the email/password combination does not " @"exist."; +static NSString *const kFIRAuthErrorMessageMissingPasskeyEnrollment = + @"Cannot find the passkey linked to the current account."; + /** @var FIRAuthErrorDescription @brief The error descrioption, based on the error code. @remarks No default case so that we get a compiler warning if a new value was added to the enum. @@ -814,6 +817,8 @@ return kFIRAuthErrorMessageInvalidReqType; case FIRAuthErrorCodeRecaptchaSDKNotLinked: return kFIRAuthErrorMessageRecaptchaSDKNotLinked; + case FIRAuthErrorCodePasskeyEnrollmentNotFound: + return kFIRAuthErrorMessageMissingPasskeyEnrollment; } } @@ -1001,6 +1006,8 @@ return @"ERROR_INVALID_REQ_TYPE"; case FIRAuthErrorCodeRecaptchaSDKNotLinked: return @"ERROR_RECAPTCHA_SDK_NOT_LINKED"; + case FIRAuthErrorCodePasskeyEnrollmentNotFound: + return @"ERROR_PASSKEY_ENROLLMENT_NOT_FOUND"; } } @@ -1239,6 +1246,10 @@ + (NSError *)userNotFoundErrorWithMessage:(nullable NSString *)message { return [self errorWithCode:FIRAuthInternalErrorCodeUserNotFound message:message]; } ++ (NSError *)passkeyEnrollmentNotFoundError { + return [self errorWithCode:FIRAuthInternalPasskeyEnrollmentNotFound]; +} + + (NSError *)invalidAPIKeyError { return [self errorWithCode:FIRAuthInternalErrorCodeInvalidAPIKey]; } diff --git a/FirebaseAuth/Sources/Utilities/FIRAuthInternalErrors.h b/FirebaseAuth/Sources/Utilities/FIRAuthInternalErrors.h index 82b99b6487d..218518f738e 100644 --- a/FirebaseAuth/Sources/Utilities/FIRAuthInternalErrors.h +++ b/FirebaseAuth/Sources/Utilities/FIRAuthInternalErrors.h @@ -175,6 +175,12 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) { */ FIRAuthInternalErrorCodeUserNotFound = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeUserNotFound, + /** @var FIRAuthInternalPasskeyEnrollmentNotFound + @brief Indicates the given credential ID doesn't exist for passkey withdrawal. + */ + FIRAuthInternalPasskeyEnrollmentNotFound = FIRAuthPublicErrorCodeFlag | + FIRAuthErrorCodePasskeyEnrollmentNotFound, + /** @var FIRAuthInternalErrorCodeInvalidAPIKey @brief Indicates an invalid API Key was supplied in the request. */ diff --git a/FirebaseAuth/Tests/Sample/AuthSample.xcodeproj/project.pbxproj b/FirebaseAuth/Tests/Sample/AuthSample.xcodeproj/project.pbxproj index 8eff016dc36..b028be6e69d 100644 --- a/FirebaseAuth/Tests/Sample/AuthSample.xcodeproj/project.pbxproj +++ b/FirebaseAuth/Tests/Sample/AuthSample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0274FD322BB5DCF000DE5140 /* MainViewController+Passkey.m in Sources */ = {isa = PBXBuildFile; fileRef = 0274FD312BB5DCF000DE5140 /* MainViewController+Passkey.m */; }; 400283EA23EA254B0006A298 /* MainViewController+MultiFactor.m in Sources */ = {isa = PBXBuildFile; fileRef = 400283E923EA254A0006A298 /* MainViewController+MultiFactor.m */; }; DE1865AC245B879B00F8AD70 /* TestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1865AB245B879B00F8AD70 /* TestsBase.swift */; }; DE1865AE245B8A1400F8AD70 /* AnonymousTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1865AD245B8A1400F8AD70 /* AnonymousTests.swift */; }; @@ -85,6 +86,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0274FD302BB5DCF000DE5140 /* MainViewController+Passkey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MainViewController+Passkey.h"; sourceTree = ""; }; + 0274FD312BB5DCF000DE5140 /* MainViewController+Passkey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MainViewController+Passkey.m"; sourceTree = ""; }; 400283E823EA254A0006A298 /* MainViewController+MultiFactor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MainViewController+MultiFactor.h"; sourceTree = ""; }; 400283E923EA254A0006A298 /* MainViewController+MultiFactor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MainViewController+MultiFactor.m"; sourceTree = ""; }; DE1865AB245B879B00F8AD70 /* TestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsBase.swift; sourceTree = ""; }; @@ -277,6 +280,8 @@ DE800B4222A2F8AF00AC9A23 /* MainViewController+GameCenter.h */, DE800B1922A2F8AF00AC9A23 /* MainViewController+GameCenter.m */, DE800B2122A2F8AF00AC9A23 /* MainViewController+Google.h */, + 0274FD302BB5DCF000DE5140 /* MainViewController+Passkey.h */, + 0274FD312BB5DCF000DE5140 /* MainViewController+Passkey.m */, DE800B3922A2F8AF00AC9A23 /* MainViewController+Google.m */, DE800B3022A2F8AF00AC9A23 /* MainViewController+Internal.h */, 400283E823EA254A0006A298 /* MainViewController+MultiFactor.h */, @@ -534,6 +539,7 @@ DE800B5A22A2F8AF00AC9A23 /* MainViewController+App.m in Sources */, DE800B6222A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.m in Sources */, DE800B6122A2F8AF00AC9A23 /* ApplicationDelegate.m in Sources */, + 0274FD322BB5DCF000DE5140 /* MainViewController+Passkey.m in Sources */, DE800B4A22A2F8AF00AC9A23 /* GoogleAuthProvider.m in Sources */, DE800B5B22A2F8AF00AC9A23 /* MainViewController+OAuth.m in Sources */, DE800B6022A2F8AF00AC9A23 /* FacebookAuthProvider.m in Sources */, diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m index 194a54064c4..c25f72a78ee 100644 --- a/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController+OAuth.m @@ -434,53 +434,76 @@ - (void)revokeAppleTokenAndDeleteUser { } - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) { - ASAuthorizationAppleIDCredential* appleIDCredential = authorization.credential; - NSString *IDToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]]; - FIROAuthCredential *credential = - [FIROAuthProvider appleCredentialWithIDToken:IDToken - rawNonce:self.appleRawNonce - fullName:appleIDCredential.fullName]; - - if ([appleIDCredential.state isEqualToString:@"signIn"]) { - [FIRAuth.auth signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { - if (!error) { - NSLog(@"%@", authResult.description); - } else { - NSLog(@"%@", error.description); - } - }]; - } else if ([appleIDCredential.state isEqualToString:@"link"]) { - [FIRAuth.auth.currentUser linkWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { - if (!error) { - NSLog(@"%@", authResult.description); - } else { - NSLog(@"%@", error.description); - } - }]; - } else if ([appleIDCredential.state isEqualToString:@"reauth"]) { - [FIRAuth.auth.currentUser reauthenticateWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { - if (!error) { - NSLog(@"%@", authResult.description); - } else { - NSLog(@"%@", error.description); - } - }]; - } else if ([appleIDCredential.state isEqualToString:@"revokeAppleTokenAndDeleteUser"]) { - NSString *code = [[NSString alloc] initWithData:appleIDCredential.authorizationCode encoding:NSUTF8StringEncoding]; + if (@available(iOS 16.0, *)) { + if ([authorization.credential isKindOfClass: [ASAuthorizationPlatformPublicKeyCredentialRegistration class]]) { + ASAuthorizationPlatformPublicKeyCredentialRegistration *platformCredential = (ASAuthorizationPlatformPublicKeyCredentialRegistration*) authorization.credential; + FIRUser *user = FIRAuth.auth.currentUser; - [FIRAuth.auth revokeTokenWithAuthorizationCode:code completion:^(NSError * _Nullable error) { + [user finalizePasskeyEnrollmentWithPlatformCredential:platformCredential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { + [self log:[NSString stringWithFormat:@"Passkey Enrollment succeed with uid: %@", authResult.user.uid] ]; + [self showTypicalUIForUserUpdateResultsWithTitle:@"Enrollment with passkey" error:error]; + }]; + + } else if ([authorization.credential isKindOfClass: [ASAuthorizationPlatformPublicKeyCredentialAssertion class]]) { + ASAuthorizationPlatformPublicKeyCredentialAssertion *platformCredential = (ASAuthorizationPlatformPublicKeyCredentialAssertion*) authorization.credential; + [[AppManager auth] finalizePasskeySignInWithPlatformCredential:platformCredential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { + [self log:[NSString stringWithFormat:@"Passkey sign-in succeed with uid: %@", authResult.user.uid]]; + [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign in with passkey" error:error]; + + }]; + } + } else if ([authorization.credential isKindOfClass: [ASAuthorizationAppleIDCredential class]]) { + ASAuthorizationAppleIDCredential* appleIDCredential = authorization.credential; + NSString *IDToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]]; + FIROAuthCredential *credential = + [FIROAuthProvider appleCredentialWithIDToken:IDToken + rawNonce:self.appleRawNonce + fullName:appleIDCredential.fullName]; + + if ([appleIDCredential.state isEqualToString:@"signIn"]) { + [FIRAuth.auth signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, 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]; - }]; + NSLog(@"%@", authResult.description); } else { NSLog(@"%@", error.description); } - }]; + }]; + } else if ([appleIDCredential.state isEqualToString:@"link"]) { + [FIRAuth.auth.currentUser linkWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { + if (!error) { + NSLog(@"%@", authResult.description); + } else { + NSLog(@"%@", error.description); + } + }]; + } else if ([appleIDCredential.state isEqualToString:@"reauth"]) { + [FIRAuth.auth.currentUser reauthenticateWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { + if (!error) { + NSLog(@"%@", authResult.description); + } else { + 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); + } + }]; + } + } else { + // More supported credential type can be added here. + [self log:@"credential type not found or OS version too low."]; } } diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.h b/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.h new file mode 100644 index 00000000000..d253e562138 --- /dev/null +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.h @@ -0,0 +1,31 @@ +/* + * 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 "MainViewController.h" + +#import "StaticContentTableViewManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MainViewController (Passkey) + +- (StaticContentTableViewSection *)passkeySection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.m b/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.m new file mode 100644 index 00000000000..8994f968d96 --- /dev/null +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController+Passkey.m @@ -0,0 +1,118 @@ +/* + * 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 "MainViewController+Passkey.h" +#import "AppManager.h" +#import "MainViewController+Internal.h" +#import + + +NS_ASSUME_NONNULL_BEGIN +@interface MainViewController () +@end + +@implementation MainViewController (Passkey) + +- (StaticContentTableViewSection *)passkeySection { + __weak typeof(self) weakSelf = self; + return [StaticContentTableViewSection sectionWithTitle:@"Passkey" cells:@[ + [StaticContentTableViewCell cellWithTitle:@"Sign Up With Passkey" + action:^{ [weakSelf passkeySignUp]; }], + [StaticContentTableViewCell cellWithTitle:@"Sign In With Passkey" + action:^{ [weakSelf passkeySignIn]; }], + [StaticContentTableViewCell cellWithTitle:@"Enroll with Passkey" + action:^{ [weakSelf passkeyEnroll]; }], + [StaticContentTableViewCell cellWithTitle:@"Unenroll with Passkey" + action:^{ [weakSelf passkeyUnenroll]; }], + ]]; +} + +- (void)passkeySignUp { + // sign in anoymously + [[AppManager auth] signInAnonymouslyWithCompletion:^(FIRAuthDataResult *_Nullable result, + NSError *_Nullable error) { + if (error) { + [self logFailure:@"sign-in anonymously failed" error:error]; + } else { + [self logSuccess:@"sign-in anonymously succeeded."]; + [self log:[NSString stringWithFormat:@"User ID : %@", result.user.uid]]; + [self passkeyEnroll]; + } + }]; +} + +- (void)passkeyEnroll { + FIRUser *user = FIRAuth.auth.currentUser; + if (!user) { + [self logFailure:@"Please sign in first." error:nil]; + return; + } + [self showTextInputPromptWithMessage:@"passkey name" + keyboardType:UIKeyboardTypeEmailAddress + completionBlock:^(BOOL userPressedOK, NSString *_Nullable passkeyName) { + if (@available(iOS 16.0, macOS 12.0, tvOS 16.0, *)) { + [user startPasskeyEnrollmentWithName:passkeyName completion:^(ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest * _Nullable request, NSError * _Nullable error) { + if (request) { + ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests: [NSMutableArray arrayWithObject:request]]; + controller.delegate = self; + controller.presentationContextProvider = self; + [controller performRequests]; + } else if (error) { + [self logFailure:@"Passkey enrollment failed" error:error]; + } + }]; + } else { + [self log:@"OS version is not supported for this action."]; + } + }]; + +} + +- (void)passkeySignIn { + if (@available(iOS 16.0, macOS 12.0, tvOS 16.0, *)) { + [[AppManager auth] startPasskeySignInWithCompletion:^(ASAuthorizationPlatformPublicKeyCredentialAssertionRequest * _Nullable request, NSError * _Nullable error) { + if (request) { + ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests: [NSMutableArray arrayWithObject:request]]; + controller.delegate = self; + controller.presentationContextProvider = self; + [controller performRequestsWithOptions:ASAuthorizationControllerRequestOptionPreferImmediatelyAvailableCredentials]; + } + }]; + } else { + [self log:@"OS version is not supported for this action."]; + } +} + +- (void)passkeyUnenroll { + FIRUser *user = FIRAuth.auth.currentUser; + if (!user) { + [self logFailure:@"Please sign in first." error:nil]; + return; + } + [self showTextInputPromptWithMessage:@"passkey credential ID" + completionBlock:^(BOOL userPressedOK, NSString *_Nullable credentialID) { + [user unenrollPasskeyWithCredentialID:credentialID completion:^(NSError * _Nullable error) { + if (error) { + [self logFailure:[NSString stringWithFormat:@"Withdraw passkey with credential ID: %@ failed", credentialID] error:error]; + } else { + [self logSuccess:[NSString stringWithFormat:@"Withdraw passkey with credential ID: %@ succeed", credentialID]]; + } + }]; + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Tests/Sample/Sample/MainViewController.m b/FirebaseAuth/Tests/Sample/Sample/MainViewController.m index 1b53c911a86..351c4d03e43 100644 --- a/FirebaseAuth/Tests/Sample/Sample/MainViewController.m +++ b/FirebaseAuth/Tests/Sample/Sample/MainViewController.m @@ -40,7 +40,8 @@ #import "UIViewController+Alerts.h" #import "UserInfoViewController.h" #import "UserTableViewCell.h" - +#import "MainViewController+Passkey.h" +#import NS_ASSUME_NONNULL_BEGIN static NSString *const kSectionTitleSettings = @"Settings"; @@ -250,6 +251,8 @@ - (void)updateTable { [weakSelf appSection], // OOB [weakSelf oobSection], + // Passkey + [weakSelf passkeySection], // Auto Tests [weakSelf autoTestsSection], ]]; @@ -638,6 +641,10 @@ - (IBAction)copyConsole:(id)sender { pasteboard.string = _consoleString ?: @""; } +- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){ + return self.view.window; +} + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Tests/Sample/Sample/UserInfoViewController.m b/FirebaseAuth/Tests/Sample/Sample/UserInfoViewController.m index fbf44ae9634..5ca334816a4 100644 --- a/FirebaseAuth/Tests/Sample/Sample/UserInfoViewController.m +++ b/FirebaseAuth/Tests/Sample/Sample/UserInfoViewController.m @@ -17,6 +17,7 @@ #import "UserInfoViewController.h" #import +#import #import #import #import "StaticContentTableViewManager.h" @@ -74,6 +75,8 @@ - (void)loadTableView { value:stringWithBool(_user.emailVerified)], [StaticContentTableViewCell cellWithTitle:@"refresh token" value:_user.refreshToken], [StaticContentTableViewCell cellWithTitle:@"multi factor" value:[self multiFactorString]], + [StaticContentTableViewCell cellWithTitle:@"passkeys" value:[self passkeysString]], + ]] ] mutableCopy]; [sections addObject:[self sectionWithUserInfo:_user]]; @@ -108,4 +111,17 @@ - (NSString *)multiFactorString { return string; } +- (NSString *)passkeysString { + NSMutableString *string = [NSMutableString string]; + + for (FIRPasskeyInfo *info in _user.enrolledPasskeys) { + [string appendString:info.name]; + [string appendString:@" - "]; + [string appendString:info.credentialID]; + [string appendString:@" "]; + } + + return string; +} + @end diff --git a/FirebaseAuth/Tests/Unit/FIRAuthTests.m b/FirebaseAuth/Tests/Unit/FIRAuthTests.m index 83cf318fc10..24db7df44a0 100644 --- a/FirebaseAuth/Tests/Unit/FIRAuthTests.m +++ b/FirebaseAuth/Tests/Unit/FIRAuthTests.m @@ -39,6 +39,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRCreateAuthURIResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIREmailLinkSignInResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetAccountInfoResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRGetOOBConfirmationCodeRequest.h" @@ -53,6 +55,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSignUpNewUserRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRSignUpNewUserResponse.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionRequest.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyAssertionResponse.h" #import "FirebaseAuth/Sources/Backend/RPC/FIRVerifyCustomTokenRequest.h" @@ -78,6 +82,10 @@ #import "FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.h" #endif // TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#import +#endif + /** @var kAPIKey @brief The fake API key. */ @@ -283,6 +291,42 @@ */ static NSString *const kFakeRecaptchaVersion = @"RecaptchaVersion"; +/** @var kRpId + @brief The fake passkey relying party identifier. + */ +static NSString *const kRpId = @"fake.rp.id"; + +/** @var kChallenge + @brief The fake passkey challenge. + */ +static NSString *const kChallenge = @"Y2hhbGxlbmdl"; // decode to "challenge" + +/** @var kCredentialID + @brief The fake passkey credentialID. + */ +static NSString *const kCredentialID = @"Y3JlZGVudGlhbGlk"; // decode to "credentialid" + +/** @var kClientDataJson + @brief The fake clientDataJson object + */ +static NSString *const kClientDataJson = @"Y2xpZW50ZGF0YWpzb24="; // decode to "clientdatajson" + +/** @var kAuthenticatorData + @brief The fake authenticatorData object + */ +static NSString *const kAuthenticatorData = + @"YXV0aGVudGljYXRvcmRhdGE="; // decode to "authenticatordata" + +/** @var kSignature + @brief The fake signature + */ +static NSString *const kSignature = @"c2lnbmF0dXJl"; // decode to "signature" + +/** @var kUserID + @brief The fake user ID / user handle + */ +static NSString *const kUserID = @"dXNlcmlk"; // decode to "userid" + #if TARGET_OS_IOS /** @class FIRAuthAppDelegate @brief Application delegate implementation to test the app delegate proxying @@ -1816,6 +1860,106 @@ - (void)testSignInAndRetrieveDataWithCustomTokenFailure { OCMVerifyAll(_mockBackend); } +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** @fn testStartPasskeySignInSuccess + @brief Tests the flow of a successful @c startPasskeySignInWithCompletion: call. + */ +- (void)testStartPasskeySignInSuccess { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeySignIn:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRStartPasskeySignInRequest *_Nullable request, + FIRStartPasskeySignInResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockStartPasskeySignInResponse = OCMClassMock([FIRStartPasskeySignInResponse class]); + OCMStub([mockStartPasskeySignInResponse rpID]).andReturn(kRpId); + OCMStub([mockStartPasskeySignInResponse challenge]).andReturn(kChallenge); + callback(mockStartPasskeySignInResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] + startPasskeySignInWithCompletion:^( + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *_Nullable request, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects([[request challenge] base64EncodedStringWithOptions:0], kChallenge); + ASAuthorizationPlatformPublicKeyCredentialProvider *provider = + (ASAuthorizationPlatformPublicKeyCredentialProvider *)[request provider]; + XCTAssertEqualObjects([provider relyingPartyIdentifier], kRpId); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testStartPasskeySignInFailure + @brief Tests the flow of a failed @c startPasskeySignInWithCompletion: call. + */ +- (void)testStartPasskeySignInFailure { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeySignIn:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] + startPasskeySignInWithCompletion:^( + ASAuthorizationPlatformPublicKeyCredentialAssertionRequest *_Nullable request, + NSError *_Nullable error) { + XCTAssertNil(request); + XCTAssertEqual(error.code, FIRAuthErrorCodeOperationNotAllowed); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testFinalizePasskeySignInFailure + @brief Tests the flow of a failed @c finalizePasskeySignInWithCompletion: call. + */ + +- (void)testFinalizePasskeySignInFailure { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend finalizePasskeySignIn:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockPlatfromCredential = + OCMClassMock([ASAuthorizationPlatformPublicKeyCredentialAssertion class]); + OCMStub([mockPlatfromCredential credentialID]) + .andReturn([[NSData alloc] initWithBase64EncodedString:kCredentialID options:0]); + OCMStub([mockPlatfromCredential rawClientDataJSON]) + .andReturn([[NSData alloc] initWithBase64EncodedString:kClientDataJson options:0]); + OCMStub([mockPlatfromCredential signature]) + .andReturn([[NSData alloc] initWithBase64EncodedString:kSignature options:0]); + OCMStub([mockPlatfromCredential userID]) + .andReturn([[NSData alloc] initWithBase64EncodedString:kUserID options:0]); + OCMStub([mockPlatfromCredential rawAuthenticatorData]) + .andReturn([[NSData alloc] initWithBase64EncodedString:kAuthenticatorData options:0]); + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] + finalizePasskeySignInWithPlatformCredential:mockPlatfromCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(authResult.user); + XCTAssertEqual(error.code, + FIRAuthErrorCodeOperationNotAllowed); + XCTAssertNotNil( + error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +#endif // #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST + #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST /** @fn testCreateUserWithEmailPasswordWithRecaptchaVerificationSuccess @brief Tests the flow of a successful @c createUserWithEmail:password:completion: call. diff --git a/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m new file mode 100644 index 00000000000..2269e1145fc --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentRequestTests.m @@ -0,0 +1,186 @@ +/* + * 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 = @"id"; + +/** + @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 = @"response"; + +/** + @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 00000000000..cafdabd7990 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeyEnrollmentResponseTests.m @@ -0,0 +1,242 @@ +/* + * 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 diff --git a/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInRequestTests.m b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInRequestTests.m new file mode 100644 index 00000000000..148ce7357b5 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInRequestTests.m @@ -0,0 +1,188 @@ +/* + * 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/FIRFinalizePasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.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/passkeySignIn:finalize?key=APIKey"; + +/** + @var kAuthenticatorAuthRespKey + @brief The key for authentication response object from the authenticator. + */ +static NSString *const kAuthenticatorAuthRespKey = @"authenticatorAuthenticationResponse"; + +/** + @var kAuthAssertionRespKey + @brief The key for authentication assertion from the authenticator. + */ +static NSString *const kAuthAssertionRespKey = @"response"; + +/** + @var kCredentialID + @brief credential ID. + */ +static NSString *const kCredentialID = @"testCredentialID"; + +/** + @var kCredentialIDKey + @brief credential ID field. + */ +static NSString *const kCredentialIDKey = @"id"; + +/** + @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 kAuthenticatorData + @brief The authenticatorData from the authenticator. + */ +static NSString *const kAuthenticatorData = @"TestAuthenticatorData"; + +/** + @var kAuthenticatorDataKey + @brief The key for authenticatorData from the authenticator. + */ +static NSString *const kAuthenticatorDataKey = @"authenticatorData"; + +/** + @var kSignature + @brief The signature from the authenticator + */ +static NSString *const kSignature = @"testSignature"; + +/** + @var kSignatureKey + @brief The key for the signature from the authenticator. + */ +static NSString *const kSignatureKey = @"signature"; + +/** + @var kUserHandle + @brief The key for the user handle. + */ +static NSString *const kUserHandle = @"testUserHandle"; + +/** + @var kUserHandleKey + @brief The key for the user handle. + */ +static NSString *const kUserHandleKey = @"userHandle"; + +/** + @class FIRFinalizePasskeySignInRequestTests + @brief Tests for @c FIRFinalizePasskeySignInRequest. + */ +@interface FIRFinalizePasskeySignInRequestTests : XCTestCase +@end + +@implementation FIRFinalizePasskeySignInRequestTests { + /** + @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)testFinalizePasskeySignInRequest { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeySignInRequest *request = + [[FIRFinalizePasskeySignInRequest alloc] initWithCredentialID:kCredentialID + clientDataJson:kRawClientDataJSON + authenticatorData:kAuthenticatorData + signature:kSignature + userID:kUserHandle + requestConfiguration:_requestConfiguration]; + + [FIRAuthBackend finalizePasskeySignIn:request + callback:^(FIRFinalizePasskeySignInResponse *_Nullable response, + NSError *_Nullable error){ + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthenticatorAuthRespKey][kCredentialIDKey], + kCredentialID); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthenticatorAuthRespKey] + [kAuthAssertionRespKey][kRawClientDataJSONKey], + kRawClientDataJSON); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAuthenticatorAuthRespKey] + [kAuthAssertionRespKey][kAuthenticatorDataKey], + kAuthenticatorData); + XCTAssertEqualObjects( + _RPCIssuer.decodedRequest[kAuthenticatorAuthRespKey][kAuthAssertionRespKey][kSignatureKey], + kSignature); + XCTAssertEqualObjects( + _RPCIssuer.decodedRequest[kAuthenticatorAuthRespKey][kAuthAssertionRespKey][kUserHandleKey], + kUserHandle); + } +} + +@end +#endif diff --git a/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInResponseTests.m b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInResponseTests.m new file mode 100644 index 00000000000..cf68fd74376 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRFinalizePasskeySignInResponseTests.m @@ -0,0 +1,244 @@ +/* + * 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/FIRFinalizePasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRFinalizePasskeySignInResponse.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 kCredentialID + @brief credential ID. + */ +static NSString *const kCredentialID = @"testCredentialID"; + +/** + @var kRawClientDataJSON + @brief CollectedClientData object from the authenticator. + */ +static NSString *const kRawClientDataJSON = @"testRawClientDataJSON"; + +/** + @var kAuthenticatorData + @brief The authenticatorData from the authenticator. + */ +static NSString *const kAuthenticatorData = @"TestAuthenticatorData"; + +/** + @var kSignature + @brief The signature from the authenticator + */ +static NSString *const kSignature = @"testSignature"; + +/** + @var kUserHandle + @brief The key for the user handle. + */ +static NSString *const kUserHandle = @"testUserHandle"; + +/** + @class FIRFinalizePasskeySignInResponseTests + @brief Tests for @c FIRFinalizePasskeySignInResponse. + */ +@interface FIRFinalizePasskeySignInResponseTests : XCTestCase +@end +@implementation FIRFinalizePasskeySignInResponseTests { + /** + @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 testSuccessfulFinalizePasskeySignInResponse + @brief This test simulates a successful @c FinalizePasskeySignin flow. + */ +- (void)testSuccessfulFinalizePasskeySignInResponse { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeySignInRequest *request = + [[FIRFinalizePasskeySignInRequest alloc] initWithCredentialID:kCredentialID + clientDataJson:kRawClientDataJSON + authenticatorData:kAuthenticatorData + signature:kSignature + userID:kUserHandle + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeySignIn:request + callback:^(FIRFinalizePasskeySignInResponse *_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 testFinalizePasskeySignInResponseMissingIDTokenError + @brief This test simulates an unexpected response returned from server in @c + FinalizePasskeySignIn flow. + */ +- (void)testFinalizePasskeySignInResponseMissingIDTokenError { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeySignInRequest *request = + [[FIRFinalizePasskeySignInRequest alloc] initWithCredentialID:kCredentialID + clientDataJson:kRawClientDataJSON + authenticatorData:kAuthenticatorData + signature:kSignature + userID:kUserHandle + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeySignIn:request + callback:^(FIRFinalizePasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"wrongkey" : @{}, + @"refreshToken" : kRefreshToken, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; + } +} +/** @fn testFinalizePasskeySignInResponseMissingRefreshTokenError + @brief This test simulates an unexpected response returned from server in @c + FinalizePasskeySignIn flow. + */ +- (void)testFinalizePasskeySignInResponseMissingRefreshTokenError { + if (@available(iOS 15.0, *)) { + FIRFinalizePasskeySignInRequest *request = + [[FIRFinalizePasskeySignInRequest alloc] initWithCredentialID:kCredentialID + clientDataJson:kRawClientDataJSON + authenticatorData:kAuthenticatorData + signature:kSignature + userID:kUserHandle + requestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRFinalizePasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend finalizePasskeySignIn:request + callback:^(FIRFinalizePasskeySignInResponse *_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 + FinalizePasskeySignIn flow. + */ +- (void)errorValidationHelperWithCallbackInvoked:(BOOL)callbackInvoked + rpcError:(NSError *)RPCError + rpcResponse:(FIRFinalizePasskeySignInResponse *)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 diff --git a/FirebaseAuth/Tests/Unit/FIRGetAccountInfoResponseTests.m b/FirebaseAuth/Tests/Unit/FIRGetAccountInfoResponseTests.m index c7531cbd383..53da73dc89e 100644 --- a/FirebaseAuth/Tests/Unit/FIRGetAccountInfoResponseTests.m +++ b/FirebaseAuth/Tests/Unit/FIRGetAccountInfoResponseTests.m @@ -125,6 +125,35 @@ */ static NSString *const kEmailVerifiedKey = @"emailVerified"; +/** @var kPasskeyInfoKey + @brief The key for the "passkeyInfo" value in the response. + */ +static NSString *const kPasskeyInfoKey = @"passkeyInfo"; + +/** + @var kNameKey + @brief The name of the field in the response JSON for name. + */ +static const NSString *kPasskeyNameKey = @"name"; + +/** + @var kCredentialIdKey + @brief The name of the field in the response JSON for credential ID. + */ +static const NSString *kCredentialIdKey = @"credentialId"; + +/** + @var kTestPasskeyName + @brief The fake name property value in the passkey response. + */ +static const NSString *kTestPasskeyName = @"testPasskeyName"; + +/** + @var kTestCredentialId + @brief The fake credential ID property value in the passkey response. + */ +static const NSString *kTestCredentialId = @"testCredentialID"; + /** @class FIRGetAccountInfoResponseTests @brief Tests for @c FIRGetAccountInfoResponse. */ @@ -208,6 +237,11 @@ - (void)testSuccessfulGetAccountInfoResponse { RPCError = error; }]; + NSArray *testPasskeyInfo = @[ @{ + kPasskeyNameKey : kTestPasskeyName, + kCredentialIdKey : kTestCredentialId, + } ]; + NSArray *users = @[ @{ kProviderUserInfoKey : @[ @{ kProviderIDkey : kTestProviderID, @@ -221,7 +255,8 @@ - (void)testSuccessfulGetAccountInfoResponse { kEmailKey : kTestEmail, kPhotoUrlKey : kTestPhotoURL, kEmailVerifiedKey : @YES, - kPasswordHashKey : kTestPasswordHash + kPasswordHashKey : kTestPasswordHash, + kPasskeyInfoKey : testPasskeyInfo, } ]; [_RPCIssuer respondWithJSON:@{ @"users" : users, @@ -238,6 +273,10 @@ - (void)testSuccessfulGetAccountInfoResponse { XCTAssertEqualObjects(RPCResponse.users[0].localID, kTestLocalID); XCTAssertEqual(RPCResponse.users[0].emailVerified, YES); XCTAssertEqualObjects(RPCResponse.users[0].passwordHash, kTestPasswordHash); + NSArray *enrolledPasskeys = RPCResponse.users[0].enrolledPasskeys; + XCTAssertEqual([enrolledPasskeys count], 1); + XCTAssertEqualObjects(enrolledPasskeys[0].name, kTestPasskeyName); + XCTAssertEqualObjects(enrolledPasskeys[0].credentialID, kTestCredentialId); NSArray *providerUserInfo = RPCResponse.users[0].providerUserInfo; if ([providerUserInfo count]) { diff --git a/FirebaseAuth/Tests/Unit/FIRSetAccountInfoRequestTests.m b/FirebaseAuth/Tests/Unit/FIRSetAccountInfoRequestTests.m index c165c61c13c..886c9803e5b 100644 --- a/FirebaseAuth/Tests/Unit/FIRSetAccountInfoRequestTests.m +++ b/FirebaseAuth/Tests/Unit/FIRSetAccountInfoRequestTests.m @@ -174,6 +174,16 @@ */ static NSString *const kTestDeleteProviders = @"TestDeleteProviders"; +/** @var kDeleteProvidersKey + @brief The key for the "deleteProvider" value in the request. + */ +static NSString *const kDeletePasskeysKey = @"deletePasskey"; + +/** @var kTestDeleteProviders + @brief The fake @c deleteProviders for testing the request. + */ +static NSString *const kTestDeletePasskeys = @"TestCredentialIDs"; + /** @var kReturnSecureTokenKey @brief The key for the "returnSecureToken" value in the request. */ @@ -271,6 +281,7 @@ - (void)testSetAccountInfoRequestOptionalFields { request.captchaResponse = kTestCaptchaResponse; request.deleteAttributes = @[ kTestDeleteAttributes ]; request.deleteProviders = @[ kTestDeleteProviders ]; + request.deletePasskeys = @[ kTestDeletePasskeys ]; [FIRAuthBackend setAccountInfo:request @@ -294,6 +305,7 @@ - (void)testSetAccountInfoRequestOptionalFields { XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteAttributesKey], @[ kTestDeleteAttributes ]); XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteProvidersKey], @[ kTestDeleteProviders ]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeletePasskeysKey], @[ kTestDeletePasskeys ]); XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); } 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 diff --git a/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInRequestTests.m b/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInRequestTests.m new file mode 100644 index 00000000000..7fbb9126e55 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInRequestTests.m @@ -0,0 +1,96 @@ +/* + * 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/FIRStartPasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.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/passkeySignIn:start?key=APIKey"; + +/** + @class FIRStartPasskeySignInRequestTests + @brief Tests for @c FIRStartPasskeySignInRequest. + */ +@interface FIRStartPasskeySignInRequestTests : XCTestCase +@end + +@implementation FIRStartPasskeySignInRequestTests { + /** + @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)testStartPasskeySignInRequest { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:_requestConfiguration]; + + [FIRAuthBackend startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error){ + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); +} + +@end +#endif diff --git a/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInResponseTests.m b/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInResponseTests.m new file mode 100644 index 00000000000..c90c4b31559 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/FIRStartPasskeySignInResponseTests.m @@ -0,0 +1,239 @@ +/* + * 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/FIRStartPasskeySignInRequest.h" +#import "FirebaseAuth/Sources/Backend/RPC/FIRStartPasskeySignInResponse.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 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 kTestRpKey + @brief the name of the "rp" property in the response. + */ +static NSString *const kRpKey = @"rpId"; + +/** + @var kTestChallengeKey + @brief the name of the "challenge" property in the response. + */ +static NSString *const kChallengeKey = @"challenge"; + +/** + @class FIRStartPasskeySignInResponseTests + @brief Tests for @c FIRStartPasskeySignInResponse. + */ +@interface FIRStartPasskeySignInResponseTests : XCTestCase +@end +@implementation FIRStartPasskeySignInResponseTests { + /** + @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 testSuccessfulStartPasskeySignInResponse + @brief This test simulates a successful @c StartPasskeySignIn flow. + */ +- (void)testSuccessfulStartPasskeySignInResponse { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialRequestOptions" : @{ + kChallengeKey : kTestChallenge, + kRpKey : kTestRpID, + }, + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.rpID, kTestRpID); + XCTAssertEqualObjects(RPCResponse.challenge, kTestChallenge); +} + +/** @fn testStartPasskeySignInResponseMissingRequestOptionsError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeySignIn flow. + */ +- (void)testStartPasskeySignInResponseMissingRequestOptionsError { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"wrongkey" : @{}, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn testStartPasskeySignInResponseMissingRpIdError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeySignIn flow. + */ +- (void)testStartPasskeySignInResponseMissingRpIdError { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialRequestOptions" : @{ + kChallengeKey : kTestChallenge, + }, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn testStartPasskeySignInResponseMissingChallengeError + @brief This test simulates an unexpected response returned from server in @c + StartPasskeySignIn flow. + */ +- (void)testStartPasskeySignInResponseMissingChallengeError { + FIRStartPasskeySignInRequest *request = + [[FIRStartPasskeySignInRequest alloc] initWithRequestConfiguration:_requestConfiguration]; + + __block BOOL callbackInvoked; + __block FIRStartPasskeySignInResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend startPasskeySignIn:request + callback:^(FIRStartPasskeySignInResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"credentialCreationOptions" : @{ + kRpKey : kTestRpID, + }, + }]; + [self errorValidationHelperWithCallbackInvoked:callbackInvoked + rpcError:RPCError + rpcResponse:RPCResponse]; +} + +/** @fn errorValidationHelperWithCallbackInvoked:rpcError:rpcResponse: + @brief Helper function to validate the unexpected response returned from server in @c + StartPasskeySignIn flow. + */ +- (void)errorValidationHelperWithCallbackInvoked:(BOOL)callbackInvoked + rpcError:(NSError *)RPCError + rpcResponse:(FIRStartPasskeySignInResponse *)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 diff --git a/FirebaseAuth/Tests/Unit/FIRUserTests.m b/FirebaseAuth/Tests/Unit/FIRUserTests.m index 30f421b68f4..7da5cc1f31e 100644 --- a/FirebaseAuth/Tests/Unit/FIRUserTests.m +++ b/FirebaseAuth/Tests/Unit/FIRUserTests.m @@ -41,6 +41,8 @@ #import "FirebaseAuth/Sources/Backend/RPC/FIRSetAccountInfoResponse.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/FIRVerifyPasswordRequest.h" @@ -60,6 +62,10 @@ #import "FirebaseAuth/Sources/AuthProvider/Phone/FIRPhoneAuthCredential_Internal.h" #endif +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#import +#endif + NS_ASSUME_NONNULL_BEGIN /** @var kAPIKey @@ -402,6 +408,47 @@ */ static NSString *const kFakeWebSignInUserInteractionFailureReason = @"fake_reason"; +/** @var kPasskeyName + @brief test passkey name. + */ +static NSString *const kPasskeyName = @"mockPasskeyName"; + +/** @var kDefaultPasskeyName + @brief default passkey name. + */ +static NSString *const kDefaultPasskeyName = @"Unnamed account (Apple)"; + +/** @var kRpId + @brief The fake passkey relying party identifier. + */ +static NSString *const kRpId = @"fake.rp.id"; + +/** @var kChallenge + @brief The fake passkey challenge. + */ +static NSString *const kChallenge = @"Y2hhbGxlbmdl"; // decode to "challenge" + +/** @var kUserID + @brief The fake user ID / user handle + */ +static NSString *const kUserID = @"dXNlcmlk"; // decode to "userid" + +/** @var kCredentialID + @brief The fake passkey credentialID. + */ +static NSString *const kCredentialID = @"Y3JlZGVudGlhbGlk"; // decode to "credentialid" + +/** @var kClientDataJson + @brief The fake clientDataJson object + */ +static NSString *const kClientDataJson = @"Y2xpZW50ZGF0YWpzb24="; // decode to "clientdatajson" + +/** @var kAttestation + @brief The fake attestationObject object + */ +static NSString *const kAttestationObject = + @"QXR0ZXN0YXRpb25PYmplY3Q="; // decode to "kAttestationObject" + /** @extention FIRSecureTokenService @brief Extends the FIRSecureTokenService class to expose one private method for testing only. */ @@ -890,6 +937,361 @@ - (void)testUpdateEmailFailure { OCMVerifyAll(_mockBackend); } +/** @fn testStartPasskeyEnrollmentSuccess + @brief Tests the flow of a successful @c startPasskeyEnrollmentWithName:completion: call + */ +- (void)testStartPasskeyEnrollmentSuccess { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeyEnrollment:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRStartPasskeyEnrollmentRequest *_Nullable request, + FIRStartPasskeyEnrollmentResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockStartPasskeyEnrollmentResponse = + OCMClassMock([FIRStartPasskeyEnrollmentResponse class]); + OCMStub([mockStartPasskeyEnrollmentResponse rpID]).andReturn(kRpId); + OCMStub([mockStartPasskeyEnrollmentResponse challenge]).andReturn(kChallenge); + OCMStub([mockStartPasskeyEnrollmentResponse userID]).andReturn(kUserID); + callback(mockStartPasskeyEnrollmentResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + [self + signInAnonymouslyWithMockGetAccountInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + startPasskeyEnrollmentWithName:kPasskeyName + completion:^( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError + *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects( + user.passkeyName, + kPasskeyName); + XCTAssertEqualObjects( + [[request challenge] + base64EncodedStringWithOptions: + 0], + kChallenge); + XCTAssertEqualObjects( + [request + relyingPartyIdentifier], + kRpId); + XCTAssertEqualObjects( + [[request userID] + base64EncodedStringWithOptions: + 0], + kUserID); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testStartPasskeyEnrollmentWithNilNameSuccess + @brief Tests the flow of a successful @c startPasskeyEnrollmentWithName:completion: call + */ +- (void)testStartPasskeyEnrollmentWithNilNameSuccess { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeyEnrollment:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRStartPasskeyEnrollmentRequest *_Nullable request, + FIRStartPasskeyEnrollmentResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockStartPasskeyEnrollmentResponse = + OCMClassMock([FIRStartPasskeyEnrollmentResponse class]); + OCMStub([mockStartPasskeyEnrollmentResponse rpID]).andReturn(kRpId); + OCMStub([mockStartPasskeyEnrollmentResponse challenge]).andReturn(kChallenge); + OCMStub([mockStartPasskeyEnrollmentResponse userID]).andReturn(kUserID); + callback(mockStartPasskeyEnrollmentResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + [self + signInAnonymouslyWithMockGetAccountInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + startPasskeyEnrollmentWithName:nil + completion:^( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError + *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects( + user.passkeyName, + kDefaultPasskeyName); + XCTAssertEqualObjects( + request.name, + kDefaultPasskeyName); + XCTAssertEqualObjects( + [[request challenge] + base64EncodedStringWithOptions: + 0], + kChallenge); + XCTAssertEqualObjects( + [request + relyingPartyIdentifier], + kRpId); + XCTAssertEqualObjects( + [[request userID] + base64EncodedStringWithOptions: + 0], + kUserID); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testStartPasskeyEnrollmentWithEmptyNameSuccess + @brief Tests the flow of a successful @c startPasskeyEnrollmentWithName:completion: call + */ +- (void)testStartPasskeyEnrollmentWithEmptyNameSuccess { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeyEnrollment:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRStartPasskeyEnrollmentRequest *_Nullable request, + FIRStartPasskeyEnrollmentResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockStartPasskeyEnrollmentResponse = + OCMClassMock([FIRStartPasskeyEnrollmentResponse class]); + OCMStub([mockStartPasskeyEnrollmentResponse rpID]).andReturn(kRpId); + OCMStub([mockStartPasskeyEnrollmentResponse challenge]).andReturn(kChallenge); + OCMStub([mockStartPasskeyEnrollmentResponse userID]).andReturn(kUserID); + callback(mockStartPasskeyEnrollmentResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + [self + signInAnonymouslyWithMockGetAccountInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + startPasskeyEnrollmentWithName:@"" + completion:^( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError + *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects( + user.passkeyName, + kDefaultPasskeyName); + XCTAssertEqualObjects( + request.name, + kDefaultPasskeyName); + XCTAssertEqualObjects( + [[request challenge] + base64EncodedStringWithOptions: + 0], + kChallenge); + + XCTAssertEqualObjects( + [request + relyingPartyIdentifier], + kRpId); + XCTAssertEqualObjects( + [[request userID] + base64EncodedStringWithOptions: + 0], + kUserID); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testStartPasskeyEnrollmentFailure + @brief Tests the flow of a failed @c startPasskeyEnrollmentWithName:completion: call + */ +- (void)testStartPasskeyEnrollmentFailure { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + OCMExpect([_mockBackend startPasskeyEnrollment:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + [self + signInAnonymouslyWithMockGetAccountInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + startPasskeyEnrollmentWithName:kPasskeyName + completion:^( + ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest + *_Nullable request, + NSError + *_Nullable error) { + XCTAssertNil(request); + XCTAssertNil( + user.passkeyName); + XCTAssertEqual( + error.code, + FIRAuthErrorCodeOperationNotAllowed); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** @fn testFinalizePasskeyEnrollmentFailure + @brief Tests the flow of a failed @c finalizePasskeyEnrollmentWithPlatformCredential:completion: + call + */ +- (void)testFinalizePasskeyEnrollmentFailure { + if (@available(iOS 15.0, tvOS 16.0, macOS 12.0, *)) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self + signInAnonymouslyWithMockGetAccountInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + OCMExpect( + [self->_mockBackend + finalizePasskeyEnrollment:[OCMArg any] + callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils + operationNotAllowedErrorWithMessage:nil]); + id mockPlatfromCredential = OCMClassMock( + [ASAuthorizationPlatformPublicKeyCredentialRegistration + class]); + OCMStub([mockPlatfromCredential credentialID]) + .andReturn([[NSData alloc] + initWithBase64EncodedString:kCredentialID + options:0]); + OCMStub([mockPlatfromCredential rawClientDataJSON]) + .andReturn([[NSData alloc] + initWithBase64EncodedString:kClientDataJson + options:0]); + OCMStub( + [mockPlatfromCredential rawAttestationObject]) + .andReturn([[NSData alloc] + initWithBase64EncodedString: + kAttestationObject + options:0]); + [user + finalizePasskeyEnrollmentWithPlatformCredential: + mockPlatfromCredential + completion:^( + FIRAuthDataResult + *_Nullable authResult, + NSError + *_Nullable error) { + XCTAssertTrue([NSThread + isMainThread]); + XCTAssertNil( + authResult + .user); + XCTAssertEqual( + error + .code, + FIRAuthErrorCodeOperationNotAllowed); + XCTAssertNotNil( + error.userInfo + [NSLocalizedDescriptionKey]); + [expectation + fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + } +} + +/** + @fn testUnenrollPasskeySuccess + @brief Tests the flow of a successful @c unenrollPasskeyWithCredentialID:completion: call + */ +- (void)testUnenrollPasskeySuccess { + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.deletePasskeys, @[ kCredentialID ]); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + OCMStub([mockSetAccountInfoResponse refreshToken]).andReturn(kRefreshToken); + OCMStub([mockSetAccountInfoResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockSetAccountInfoResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + callback(mockSetAccountInfoResponse, nil); + }); + }); + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser enrolledPasskeys]).andReturn(@[ kCredentialID ]); + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockSecureTokenService = OCMClassMock([FIRSecureTokenService class]); + OCMStub([mockSecureTokenService hasValidAccessToken]).andReturn(YES); + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + unenrollPasskeyWithCredentialID:kCredentialID + completion:^( + NSError + *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects( + user.rawAccessToken, + kAccessToken); + XCTAssertEqualObjects( + user.refreshToken, + kRefreshToken); + + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** + @fn testUnenrollPasskeyFailure + @brief Tests the flow of a failed @c unenrollPasskeyWithCredentialID:completion: call + */ +- (void)testUnenrollPasskeyFailure { + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]); + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id mockSecureTokenService = OCMClassMock([FIRSecureTokenService class]); + OCMStub([mockSecureTokenService hasValidAccessToken]).andReturn(YES); + [self + signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *_Nonnull user) { + [user + unenrollPasskeyWithCredentialID:kCredentialID + completion:^( + NSError + *_Nullable error) { + XCTAssertTrue([NSThread + isMainThread]); + XCTAssertEqual( + error.code, + FIRAuthErrorCodeOperationNotAllowed); + XCTAssertNotNil( + error.userInfo + [NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + /** @fn testUpdateEmailAutoSignOut @brief Tests the flow of a failed @c updateEmail:completion: call that automatically signs out. */ diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index f14b41f907f..e6957bfec46 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -17,6 +17,7 @@ import Foundation import XCTest +import AuthenticationServices import FirebaseAuth import FirebaseCore @@ -109,6 +110,16 @@ class AuthAPI_hOnlyTests: XCTestCase { auth.signIn(with: OAuthProvider(providerID: "abc"), uiDelegate: nil) { result, error in } #endif + if #available(iOS 15.0, macOS 12.0, tvOS 16.0, *) { + auth.startPasskeySignIn { result, error in + } + let apa = NSCoder() + auth + .finalizePasskeySignIn( + with: ASAuthorizationPlatformPublicKeyCredentialAssertion(coder: apa)! + ) { result, error in + } + } auth.signInAnonymously { result, error in } auth.signIn(withCustomToken: "abc") { result, error in @@ -171,6 +182,14 @@ class AuthAPI_hOnlyTests: XCTestCase { #if os(iOS) && !targetEnvironment(macCatalyst) try await auth.initializeRecaptchaConfig() #endif + if #available(iOS 15.0, macOS 12.0, tvOS 16.0, *) { + _ = try await auth.startPasskeySignIn() + let apa = NSCoder() + _ = try await auth + .finalizePasskeySignIn( + with: ASAuthorizationPlatformPublicKeyCredentialAssertion(coder: apa)! + ) + } _ = try await auth.signIn(with: OAuthProvider(providerID: "abc"), uiDelegate: nil) _ = try await auth.signInAnonymously() _ = try await auth.signIn(withCustomToken: "abc") @@ -461,6 +480,11 @@ class AuthAPI_hOnlyTests: XCTestCase { } #endif + func passkeyInfo(pi: PasskeyInfo) { + let _: String = pi.name + let _: String = pi.credentialID + } + func oauthCredential(credential: OAuthCredential) { if let _: String = credential.idToken, let _: String = credential.accessToken, @@ -583,6 +607,19 @@ class AuthAPI_hOnlyTests: XCTestCase { user.link(with: provider as! FederatedAuthProvider, uiDelegate: nil) { _, _ in } #endif + if #available(iOS 15.0, macOS 12.0, tvOS 16.0, *) { + user.startPasskeyEnrollment(with: "token") { _, _ in + } + let apr = NSCoder() + user + .finalizePasskeyEnrollment( + with: ASAuthorizationPlatformPublicKeyCredentialRegistration(coder: apr)! + ) { _, _ in + } + } + + user.unenrollPasskey(with: "credentialId") { _ in + } user.getIDTokenResult { _, _ in } user.getIDTokenResult(forcingRefresh: true) { _, _ in @@ -650,6 +687,15 @@ class AuthAPI_hOnlyTests: XCTestCase { try await user.reauthenticate(with: provider as! FederatedAuthProvider, uiDelegate: nil) try await user.link(with: provider as! FederatedAuthProvider, uiDelegate: nil) #endif + if #available(iOS 15.0, macOS 12.0, tvOS 16.0, *) { + try await user.startPasskeyEnrollment(with: "token") + let apr = NSCoder() + try await user + .finalizePasskeyEnrollment( + with: ASAuthorizationPlatformPublicKeyCredentialRegistration(coder: apr)! + ) + } + try await user.unenrollPasskey(with: "credentialID") _ = try await user.getIDTokenResult() _ = try await user.getIDTokenResult(forcingRefresh: true) _ = try await user.getIDToken()