Skip to content

Commit 08f43f1

Browse files
authored
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
1 parent b136d04 commit 08f43f1

File tree

8 files changed

+239
-30
lines changed

8 files changed

+239
-30
lines changed

auth/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ set(desktop_SRCS
126126
src/desktop/rpcs/reset_password_request.cc
127127
src/desktop/rpcs/secure_token_request.cc
128128
src/desktop/rpcs/set_account_info_request.cc
129+
src/desktop/rpcs/sign_up_request.cc
129130
src/desktop/rpcs/sign_up_new_user_request.cc
130131
src/desktop/rpcs/verify_assertion_request.cc
131132
src/desktop/rpcs/verify_custom_token_request.cc

auth/src/desktop/get_account_info_result.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ void GetAccountInfoResult::MergeToUser(UserView::Writer& user) const {
5959
user_impl_.has_email_password_credential;
6060
user->creation_timestamp = user_impl_.creation_timestamp;
6161
user->last_sign_in_timestamp = user_impl_.last_sign_in_timestamp;
62+
// If the account info has an email, make sure is_anonymous is false
63+
if (!user_impl_.email.empty() && user_impl_.has_email_password_credential) {
64+
user->is_anonymous = false;
65+
}
6266

6367
user.ResetUserInfos(provider_data_);
6468
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "auth/src/desktop/rpcs/sign_up_request.h"
18+
19+
#include <memory>
20+
#include <string>
21+
22+
#include "app/src/assert.h"
23+
#include "app/src/include/firebase/app.h"
24+
25+
namespace firebase {
26+
namespace auth {
27+
28+
SignUpRequest::SignUpRequest(::firebase::App& app, const char* api_key)
29+
: AuthRequest(app, request_resource_data, true) {
30+
FIREBASE_ASSERT_RETURN_VOID(api_key);
31+
32+
std::string url(
33+
"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=");
34+
url.append(api_key);
35+
set_url(url.c_str());
36+
37+
application_data_->returnSecureToken = true;
38+
}
39+
40+
std::unique_ptr<SignUpRequest>
41+
SignUpRequest::CreateLinkWithEmailAndPasswordRequest(::firebase::App& app,
42+
const char* api_key,
43+
const char* email,
44+
const char* password) {
45+
auto request =
46+
std::unique_ptr<SignUpRequest>(new SignUpRequest(app, api_key));
47+
48+
if (email) {
49+
request->application_data_->email = email;
50+
} else {
51+
LogError("No email given");
52+
}
53+
if (password) {
54+
request->application_data_->password = password;
55+
} else {
56+
LogError("No password given");
57+
}
58+
request->UpdatePostFields();
59+
return request;
60+
}
61+
62+
} // namespace auth
63+
} // namespace firebase
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_
18+
#define FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_
19+
20+
#include <memory>
21+
22+
#include "app/src/include/firebase/app.h"
23+
#include "auth/request_generated.h"
24+
#include "auth/request_resource.h"
25+
#include "auth/src/desktop/rpcs/auth_request.h"
26+
27+
namespace firebase {
28+
namespace auth {
29+
30+
// Represents the request payload for the signUp HTTP API. Use this to
31+
// upgrade anonymous accounts with email and password. The full specification of
32+
// the HTTP API can be found at
33+
// https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signUp
34+
class SignUpRequest : public AuthRequest {
35+
private:
36+
explicit SignUpRequest(::firebase::App& app, const char* api_key);
37+
static std::unique_ptr<SignUpRequest> CreateRequest(::firebase::App& app,
38+
const char* api_key);
39+
40+
public:
41+
// Initializer for linking an email and password to an account.
42+
static std::unique_ptr<SignUpRequest> CreateLinkWithEmailAndPasswordRequest(
43+
::firebase::App& app, const char* api_key, const char* email,
44+
const char* password);
45+
46+
void SetIdToken(const char* id_token) {
47+
if (id_token) {
48+
application_data_->idToken = id_token;
49+
UpdatePostFields();
50+
} else {
51+
LogError("No id token given.");
52+
}
53+
}
54+
};
55+
56+
} // namespace auth
57+
} // namespace firebase
58+
59+
#endif // FIREBASE_AUTH_SRC_DESKTOP_RPCS_SIGN_UP_REQUEST_H_

auth/src/desktop/user_desktop.cc

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "auth/src/desktop/rpcs/secure_token_response.h"
4040
#include "auth/src/desktop/rpcs/set_account_info_request.h"
4141
#include "auth/src/desktop/rpcs/set_account_info_response.h"
42+
#include "auth/src/desktop/rpcs/sign_up_request.h"
4243
#include "auth/src/desktop/rpcs/verify_assertion_request.h"
4344
#include "auth/src/desktop/rpcs/verify_assertion_response.h"
4445
#include "auth/src/desktop/rpcs/verify_password_request.h"
@@ -256,6 +257,47 @@ void TriggerSaveUserFlow(AuthData* const auth_data) {
256257
}
257258
}
258259

260+
template <typename ResponseT, typename ResultT>
261+
void PerformSignUpFlow(AuthDataHandle<ResultT, SignUpRequest>* const handle) {
262+
FIREBASE_ASSERT_RETURN_VOID(handle && handle->request);
263+
264+
const auto response = GetResponse<ResponseT>(*handle->request);
265+
const AuthenticationResult auth_response =
266+
CompleteSignInFlow(handle->auth_data, response);
267+
268+
if (auth_response.IsValid()) {
269+
const AuthResult auth_result =
270+
auth_response.SetAsCurrentUser(handle->auth_data);
271+
// The usual SignIn flow doesn't trigger this, but since this is used
272+
// to upgrade anonymous accounts, it is needed for SignUp
273+
NotifyIdTokenListeners(handle->auth_data);
274+
CompletePromise(&handle->promise, auth_result);
275+
} else {
276+
FailPromise(&handle->promise, auth_response.error());
277+
}
278+
}
279+
280+
template <typename ResponseT, typename ResultT>
281+
void PerformSignUpFlow_DEPRECATED(
282+
AuthDataHandle<ResultT, SignUpRequest>* const handle) {
283+
FIREBASE_ASSERT_RETURN_VOID(handle && handle->request);
284+
285+
const auto response = GetResponse<ResponseT>(*handle->request);
286+
const AuthenticationResult auth_response =
287+
CompleteSignInFlow(handle->auth_data, response);
288+
289+
if (auth_response.IsValid()) {
290+
const SignInResult sign_in_result =
291+
auth_response.SetAsCurrentUser_DEPRECATED(handle->auth_data);
292+
// The usual SignIn flow doesn't trigger this, but since this is used
293+
// to upgrade anonymous accounts, it is needed for SignUp
294+
NotifyIdTokenListeners(handle->auth_data);
295+
CompletePromise(&handle->promise, sign_in_result);
296+
} else {
297+
FailPromise(&handle->promise, auth_response.error());
298+
}
299+
}
300+
259301
template <typename ResultT>
260302
void PerformSetAccountInfoFlow(
261303
AuthDataHandle<ResultT, SetAccountInfoRequest>* const handle) {
@@ -306,14 +348,14 @@ Future<ResultT> DoLinkWithEmailAndPassword(
306348
const EmailAuthCredential* email_credential =
307349
GetEmailCredential(raw_credential_impl);
308350

309-
typedef SetAccountInfoRequest RequestT;
351+
typedef SignUpRequest RequestT;
310352
auto request = RequestT::CreateLinkWithEmailAndPasswordRequest(
311353
*auth_data->app, GetApiKey(*auth_data),
312354
email_credential->GetEmail().c_str(),
313355
email_credential->GetPassword().c_str());
314356

315357
return CallAsyncWithFreshToken(auth_data, promise, std::move(request),
316-
PerformSetAccountInfoFlow<ResultT>);
358+
PerformSignUpFlow<SignUpNewUserResponse>);
317359
}
318360

319361
// Calls setAccountInfo endpoint to link the current user with the given email
@@ -329,14 +371,15 @@ Future<ResultT> DoLinkWithEmailAndPassword_DEPRECATED(
329371
const EmailAuthCredential* email_credential =
330372
GetEmailCredential(raw_credential_impl);
331373

332-
typedef SetAccountInfoRequest RequestT;
374+
typedef SignUpRequest RequestT;
333375
auto request = RequestT::CreateLinkWithEmailAndPasswordRequest(
334376
*auth_data->app, GetApiKey(*auth_data),
335377
email_credential->GetEmail().c_str(),
336378
email_credential->GetPassword().c_str());
337379

338-
return CallAsyncWithFreshToken(auth_data, promise, std::move(request),
339-
PerformSetAccountInfoFlow_DEPRECATED<ResultT>);
380+
return CallAsyncWithFreshToken(
381+
auth_data, promise, std::move(request),
382+
PerformSignUpFlow_DEPRECATED<SignUpNewUserResponse>);
340383
}
341384

342385
// Checks that the given provider wasn't already linked to the currently

auth/tests/desktop/user_desktop_test.cc

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,32 @@ TEST_F(UserDesktopTest, TestLinkWithCredential_OauthCredential) {
567567
}
568568

569569
TEST_F(UserDesktopTest, TestLinkWithCredential_EmailCredential) {
570-
InitializeConfigWithAFake(GetUrlForApi(API_KEY, "setAccountInfo"),
571-
FakeSetAccountInfoResponse());
570+
FakeSetT fakes;
571+
const auto api_url =
572+
std::string(
573+
"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=") +
574+
API_KEY;
575+
fakes[api_url] =
576+
FakeSuccessfulResponse("SignupNewUserResponse",
577+
" \"idToken\": \"idtoken123\","
578+
" \"refreshToken\": \"refreshtoken123\","
579+
" \"expiresIn\": \"3600\","
580+
" \"localId\": \"localid123\"");
581+
fakes[GetUrlForApi(API_KEY, "getAccountInfo")] =
582+
FakeSuccessfulResponse("GetAccountInfoResponse",
583+
" \"users\": ["
584+
" {"
585+
" \"localId\": \"localid123\","
586+
" \"lastLoginAt\": \"123\","
587+
" \"createdAt\": \"456\","
588+
" \"email\": \"[email protected]\","
589+
" \"idToken\": \"new_fake_token\","
590+
" \"passwordHash\": \"new_fake_hash\","
591+
" \"emailVerified\": false," +
592+
GetFakeProviderInfo() +
593+
" }"
594+
" ]");
595+
InitializeConfigWithFakes(fakes);
572596

573597
// Response contains a new ID token, but user should have stayed the same.
574598
id_token_listener.ExpectChanges(1);
@@ -844,18 +868,6 @@ TEST_F(UserDesktopTestSignOutOnError, Unlink) {
844868
sem_.Wait();
845869
}
846870

847-
TEST_F(UserDesktopTestSignOutOnError, LinkWithEmail) {
848-
CheckSignOutIfUserIsInvalid(
849-
GetUrlForApi(API_KEY, "setAccountInfo"), "USER_NOT_FOUND",
850-
kAuthErrorUserNotFound, [&] {
851-
sem_.Post();
852-
return firebase_user_->LinkWithCredential_DEPRECATED(
853-
EmailAuthProvider::GetCredential("[email protected]",
854-
"fake_password"));
855-
});
856-
sem_.Wait();
857-
}
858-
859871
TEST_F(UserDesktopTestSignOutOnError, LinkWithOauthCredential) {
860872
CheckSignOutIfUserIsInvalid(
861873
GetUrlForApi(API_KEY, "verifyAssertion"), "USER_NOT_FOUND",

auth/tests/user_test.cc

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -402,18 +402,42 @@ TEST_F(UserTest, TestSendEmailVerification) {
402402
}
403403

404404
TEST_F(UserTest, TestLinkWithCredential) {
405-
const std::string config =
406-
std::string(
407-
"{"
408-
" config:["
409-
" {fake:'FirebaseUser.linkWithCredential', "
410-
"futuregeneric:{ticker:1}},"
411-
" {fake:'FIRUser.linkWithCredential:completion:',"
412-
" futuregeneric:{ticker:1}},") +
413-
SET_ACCOUNT_INFO_SUCCESSFUL_RESPONSE +
405+
// Under the hood, since this is linking an email/password,
406+
// it is expecting a signUp call, followed by a getAccountInfo.
407+
firebase::testing::cppsdk::ConfigSet(
408+
"{"
409+
" config:["
410+
" {fake:'FirebaseUser.linkWithCredential', "
411+
"futuregeneric:{ticker:1}},"
412+
" {fake:'FIRUser.linkWithCredential:completion:',"
413+
" futuregeneric:{ticker:1}},"
414+
" "
415+
"{fake:'https://identitytoolkit.googleapis.com/v1/"
416+
"accounts:signUp?key=not_a_real_api_key',"
417+
" httpresponse: {"
418+
" header: ['HTTP/1.1 200 Ok','Server:mock server 101'],"
419+
" body: ['{"
420+
" \"kind\": \"identitytoolkit#SignupNewUserResponse\","
421+
" \"idToken\": \"idtoken123\","
422+
" \"refreshToken\": \"refreshtoken123\","
423+
" \"expiresIn\": \"3600\","
424+
" \"localId\": \"localid123\""
425+
"}',]"
426+
" }"
427+
" },"
428+
" {fake:'https://www.googleapis.com/identitytoolkit/v3/relyingparty/"
429+
"getAccountInfo?key=not_a_real_api_key',"
430+
" httpresponse: {"
431+
" header: ['HTTP/1.1 200 Ok','Server:mock server 101'],"
432+
" body: ['{"
433+
" \"users\": [{"
434+
" \"localId\": \"localid123\""
435+
" }]}',"
436+
" ]"
437+
" }"
438+
" }"
414439
" ]"
415-
"}";
416-
firebase::testing::cppsdk::ConfigSet(config.c_str());
440+
"}");
417441

418442
Future<User*> result = firebase_user_->LinkWithCredential_DEPRECATED(
419443
EmailAuthProvider::GetCredential("[email protected]", "pw"));

release_build_files/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,9 @@ code.
638638
- General (Android): Update to Firebase Android BoM version 32.3.1.
639639
- General (iOS): Update to Firebase Cocoapods version 10.17.0.
640640
- Analytics: Updated the consent management API to include new consent signals.
641+
- Auth: Fix a bug where anonymous account can't be linked with
642+
email password credential. For background, see
643+
[Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection#overview)
641644
- GMA (Android) Updated dependency to play-services-ads version 22.4.0.
642645

643646
### 11.6.0

0 commit comments

Comments
 (0)