From 08f43f1123b46d4be51d0b6143353fc7ecf364aa Mon Sep 17 00:00:00 2001 From: a-maurice Date: Tue, 19 Dec 2023 16:26:28 -0800 Subject: [PATCH] Fix issue with linking email to anonymous accounts on desktop (#1497) * Fix issue with linking email to anonymous accounts * Update the Auth unit test for linking credentials * Fix lint errors around include memory * Format file * Update more unit tests * Formatting * Reuse more of the regular SignInFlow logic --- auth/CMakeLists.txt | 1 + auth/src/desktop/get_account_info_result.cc | 4 ++ auth/src/desktop/rpcs/sign_up_request.cc | 63 +++++++++++++++++++++ auth/src/desktop/rpcs/sign_up_request.h | 59 +++++++++++++++++++ auth/src/desktop/user_desktop.cc | 53 +++++++++++++++-- auth/tests/desktop/user_desktop_test.cc | 40 ++++++++----- auth/tests/user_test.cc | 46 +++++++++++---- release_build_files/readme.md | 3 + 8 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 auth/src/desktop/rpcs/sign_up_request.cc create mode 100644 auth/src/desktop/rpcs/sign_up_request.h diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt index 2dc7c4e255..5ffbc2ec71 100644 --- a/auth/CMakeLists.txt +++ b/auth/CMakeLists.txt @@ -126,6 +126,7 @@ set(desktop_SRCS src/desktop/rpcs/reset_password_request.cc src/desktop/rpcs/secure_token_request.cc src/desktop/rpcs/set_account_info_request.cc + src/desktop/rpcs/sign_up_request.cc src/desktop/rpcs/sign_up_new_user_request.cc src/desktop/rpcs/verify_assertion_request.cc src/desktop/rpcs/verify_custom_token_request.cc diff --git a/auth/src/desktop/get_account_info_result.cc b/auth/src/desktop/get_account_info_result.cc index cda3117b59..e2c3eb1e89 100644 --- a/auth/src/desktop/get_account_info_result.cc +++ b/auth/src/desktop/get_account_info_result.cc @@ -59,6 +59,10 @@ void GetAccountInfoResult::MergeToUser(UserView::Writer& user) const { user_impl_.has_email_password_credential; user->creation_timestamp = user_impl_.creation_timestamp; user->last_sign_in_timestamp = user_impl_.last_sign_in_timestamp; + // If the account info has an email, make sure is_anonymous is false + if (!user_impl_.email.empty() && user_impl_.has_email_password_credential) { + user->is_anonymous = false; + } user.ResetUserInfos(provider_data_); } diff --git a/auth/src/desktop/rpcs/sign_up_request.cc b/auth/src/desktop/rpcs/sign_up_request.cc new file mode 100644 index 0000000000..c11537c300 --- /dev/null +++ b/auth/src/desktop/rpcs/sign_up_request.cc @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "auth/src/desktop/rpcs/sign_up_request.h" + +#include +#include + +#include "app/src/assert.h" +#include "app/src/include/firebase/app.h" + +namespace firebase { +namespace auth { + +SignUpRequest::SignUpRequest(::firebase::App& app, const char* api_key) + : AuthRequest(app, request_resource_data, true) { + FIREBASE_ASSERT_RETURN_VOID(api_key); + + std::string url( + "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key="); + url.append(api_key); + set_url(url.c_str()); + + application_data_->returnSecureToken = true; +} + +std::unique_ptr +SignUpRequest::CreateLinkWithEmailAndPasswordRequest(::firebase::App& app, + const char* api_key, + const char* email, + const char* password) { + auto request = + std::unique_ptr(new SignUpRequest(app, api_key)); + + if (email) { + request->application_data_->email = email; + } else { + LogError("No email given"); + } + if (password) { + request->application_data_->password = password; + } else { + LogError("No password given"); + } + request->UpdatePostFields(); + return request; +} + +} // namespace auth +} // namespace firebase diff --git a/auth/src/desktop/rpcs/sign_up_request.h b/auth/src/desktop/rpcs/sign_up_request.h new file mode 100644 index 0000000000..9a0ca404ea --- /dev/null +++ b/auth/src/desktop/rpcs/sign_up_request.h @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#ifndef FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_ +#define FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_ + +#include + +#include "app/src/include/firebase/app.h" +#include "auth/request_generated.h" +#include "auth/request_resource.h" +#include "auth/src/desktop/rpcs/auth_request.h" + +namespace firebase { +namespace auth { + +// Represents the request payload for the signUp HTTP API. Use this to +// upgrade anonymous accounts with email and password. The full specification of +// the HTTP API can be found at +// https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signUp +class SignUpRequest : public AuthRequest { + private: + explicit SignUpRequest(::firebase::App& app, const char* api_key); + static std::unique_ptr CreateRequest(::firebase::App& app, + const char* api_key); + + public: + // Initializer for linking an email and password to an account. + static std::unique_ptr CreateLinkWithEmailAndPasswordRequest( + ::firebase::App& app, const char* api_key, const char* email, + const char* password); + + void SetIdToken(const char* id_token) { + if (id_token) { + application_data_->idToken = id_token; + UpdatePostFields(); + } else { + LogError("No id token given."); + } + } +}; + +} // namespace auth +} // namespace firebase + +#endif // FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_ diff --git a/auth/src/desktop/user_desktop.cc b/auth/src/desktop/user_desktop.cc index cefd64a799..9a6b5a96ad 100644 --- a/auth/src/desktop/user_desktop.cc +++ b/auth/src/desktop/user_desktop.cc @@ -39,6 +39,7 @@ #include "auth/src/desktop/rpcs/secure_token_response.h" #include "auth/src/desktop/rpcs/set_account_info_request.h" #include "auth/src/desktop/rpcs/set_account_info_response.h" +#include "auth/src/desktop/rpcs/sign_up_request.h" #include "auth/src/desktop/rpcs/verify_assertion_request.h" #include "auth/src/desktop/rpcs/verify_assertion_response.h" #include "auth/src/desktop/rpcs/verify_password_request.h" @@ -256,6 +257,47 @@ void TriggerSaveUserFlow(AuthData* const auth_data) { } } +template +void PerformSignUpFlow(AuthDataHandle* const handle) { + FIREBASE_ASSERT_RETURN_VOID(handle && handle->request); + + const auto response = GetResponse(*handle->request); + const AuthenticationResult auth_response = + CompleteSignInFlow(handle->auth_data, response); + + if (auth_response.IsValid()) { + const AuthResult auth_result = + auth_response.SetAsCurrentUser(handle->auth_data); + // The usual SignIn flow doesn't trigger this, but since this is used + // to upgrade anonymous accounts, it is needed for SignUp + NotifyIdTokenListeners(handle->auth_data); + CompletePromise(&handle->promise, auth_result); + } else { + FailPromise(&handle->promise, auth_response.error()); + } +} + +template +void PerformSignUpFlow_DEPRECATED( + AuthDataHandle* const handle) { + FIREBASE_ASSERT_RETURN_VOID(handle && handle->request); + + const auto response = GetResponse(*handle->request); + const AuthenticationResult auth_response = + CompleteSignInFlow(handle->auth_data, response); + + if (auth_response.IsValid()) { + const SignInResult sign_in_result = + auth_response.SetAsCurrentUser_DEPRECATED(handle->auth_data); + // The usual SignIn flow doesn't trigger this, but since this is used + // to upgrade anonymous accounts, it is needed for SignUp + NotifyIdTokenListeners(handle->auth_data); + CompletePromise(&handle->promise, sign_in_result); + } else { + FailPromise(&handle->promise, auth_response.error()); + } +} + template void PerformSetAccountInfoFlow( AuthDataHandle* const handle) { @@ -306,14 +348,14 @@ Future DoLinkWithEmailAndPassword( const EmailAuthCredential* email_credential = GetEmailCredential(raw_credential_impl); - typedef SetAccountInfoRequest RequestT; + typedef SignUpRequest RequestT; auto request = RequestT::CreateLinkWithEmailAndPasswordRequest( *auth_data->app, GetApiKey(*auth_data), email_credential->GetEmail().c_str(), email_credential->GetPassword().c_str()); return CallAsyncWithFreshToken(auth_data, promise, std::move(request), - PerformSetAccountInfoFlow); + PerformSignUpFlow); } // Calls setAccountInfo endpoint to link the current user with the given email @@ -329,14 +371,15 @@ Future DoLinkWithEmailAndPassword_DEPRECATED( const EmailAuthCredential* email_credential = GetEmailCredential(raw_credential_impl); - typedef SetAccountInfoRequest RequestT; + typedef SignUpRequest RequestT; auto request = RequestT::CreateLinkWithEmailAndPasswordRequest( *auth_data->app, GetApiKey(*auth_data), email_credential->GetEmail().c_str(), email_credential->GetPassword().c_str()); - return CallAsyncWithFreshToken(auth_data, promise, std::move(request), - PerformSetAccountInfoFlow_DEPRECATED); + return CallAsyncWithFreshToken( + auth_data, promise, std::move(request), + PerformSignUpFlow_DEPRECATED); } // Checks that the given provider wasn't already linked to the currently diff --git a/auth/tests/desktop/user_desktop_test.cc b/auth/tests/desktop/user_desktop_test.cc index 13546b4e1e..f22554d122 100644 --- a/auth/tests/desktop/user_desktop_test.cc +++ b/auth/tests/desktop/user_desktop_test.cc @@ -567,8 +567,32 @@ TEST_F(UserDesktopTest, TestLinkWithCredential_OauthCredential) { } TEST_F(UserDesktopTest, TestLinkWithCredential_EmailCredential) { - InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"), - FakeSetAccountInfoResponse()); + FakeSetT fakes; + const auto api_url = + std::string( + "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=") + + API_KEY; + fakes[api_url] = + FakeSuccessfulResponse("SignupNewUserResponse", + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\""); + fakes[GetUrlForApi(API_KEY, "getAccountInfo")] = + FakeSuccessfulResponse("GetAccountInfoResponse", + " \"users\": [" + " {" + " \"localId\": \"localid123\"," + " \"lastLoginAt\": \"123\"," + " \"createdAt\": \"456\"," + " \"email\": \"new_fake_email@example.com\"," + " \"idToken\": \"new_fake_token\"," + " \"passwordHash\": \"new_fake_hash\"," + " \"emailVerified\": false," + + GetFakeProviderInfo() + + " }" + " ]"); + InitializeConfigWithFakes(fakes); // Response contains a new ID token, but user should have stayed the same. id_token_listener.ExpectChanges(1); @@ -844,18 +868,6 @@ TEST_F(UserDesktopTestSignOutOnError, Unlink) { sem_.Wait(); } -TEST_F(UserDesktopTestSignOutOnError, LinkWithEmail) { - CheckSignOutIfUserIsInvalid( - GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND", - kAuthErrorUserNotFound, [&] { - sem_.Post(); - return firebase_user_->LinkWithCredential_DEPRECATED( - EmailAuthProvider::GetCredential("fake_email@example.com", - "fake_password")); - }); - sem_.Wait(); -} - TEST_F(UserDesktopTestSignOutOnError, LinkWithOauthCredential) { CheckSignOutIfUserIsInvalid( GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND", diff --git a/auth/tests/user_test.cc b/auth/tests/user_test.cc index 52cd8e6391..ca76f34f33 100644 --- a/auth/tests/user_test.cc +++ b/auth/tests/user_test.cc @@ -402,18 +402,42 @@ TEST_F(UserTest, TestSendEmailVerification) { } TEST_F(UserTest, TestLinkWithCredential) { - const std::string config = - std::string( - "{" - " config:[" - " {fake:'FirebaseUser.linkWithCredential', " - "futuregeneric:{ticker:1}}," - " {fake:'FIRUser.linkWithCredential:completion:'," - " futuregeneric:{ticker:1}},") + - SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE + + // Under the hood, since this is linking an email/password, + // it is expecting a signUp call, followed by a getAccountInfo. + firebase::testing::cppsdk::ConfigSet( + "{" + " config:[" + " {fake:'FirebaseUser.linkWithCredential', " + "futuregeneric:{ticker:1}}," + " {fake:'FIRUser.linkWithCredential:completion:'," + " futuregeneric:{ticker:1}}," + " " + "{fake:'https://identitytoolkit.googleapis.com/v1/" + "accounts:signUp?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"kind\": \"identitytoolkit#SignupNewUserResponse\"," + " \"idToken\": \"idtoken123\"," + " \"refreshToken\": \"refreshtoken123\"," + " \"expiresIn\": \"3600\"," + " \"localId\": \"localid123\"" + "}',]" + " }" + " }," + " {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/" + "getAccountInfo?key=not_a_real_api_key'," + " httpresponse: {" + " header: ['HTTP/1.1 200 Ok','Server:mock server 101']," + " body: ['{" + " \"users\": [{" + " \"localId\": \"localid123\"" + " }]}'," + " ]" + " }" + " }" " ]" - "}"; - firebase::testing::cppsdk::ConfigSet(config.c_str()); + "}"); Future result = firebase_user_->LinkWithCredential_DEPRECATED( EmailAuthProvider::GetCredential("i@email.com", "pw")); diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 1511e60bb0..301c4c106d 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -638,6 +638,9 @@ code. - General (Android): Update to Firebase Android BoM version 32.3.1. - General (iOS): Update to Firebase Cocoapods version 10.17.0. - Analytics: Updated the consent management API to include new consent signals. + - Auth: Fix a bug where anonymous account can't be linked with + email password credential. For background, see + [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection#overview) - GMA (Android) Updated dependency to play-services-ads version 22.4.0. ### 11.6.0