Skip to content

Commit 33bf95d

Browse files
authored
feat(auth): Add ability to link a federated id with the UpdateUser() method. (#344)
1 parent 11fd169 commit 33bf95d

File tree

4 files changed

+463
-47
lines changed

4 files changed

+463
-47
lines changed

auth/import_users.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,8 @@ func (u *UserToImport) validatedUserInfo() (map[string]interface{}, error) {
236236

237237
if providers, ok := info["providerUserInfo"]; ok {
238238
for _, p := range providers.([]*UserProvider) {
239-
if p.UID == "" {
240-
return nil, fmt.Errorf("user provdier must specify a uid")
241-
}
242-
if p.ProviderID == "" {
243-
return nil, fmt.Errorf("user provider must specify a provider ID")
239+
if err := validateProviderUserInfo(p); err != nil {
240+
return nil, err
244241
}
245242
}
246243
}

auth/user_mgt.go

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import (
3030
)
3131

3232
const (
33-
maxLenPayloadCC = 1000
34-
defaultProviderID = "firebase"
33+
maxLenPayloadCC = 1000
34+
defaultProviderID = "firebase"
35+
idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1"
3536

3637
// Maximum number of users allowed to batch get at a time.
3738
maxGetAccountsBatchSize = 100
@@ -217,6 +218,34 @@ func (u *UserToUpdate) PhotoURL(url string) *UserToUpdate {
217218
return u.set("photoUrl", url)
218219
}
219220

221+
// ProviderToLink links this user to the specified provider.
222+
//
223+
// Linking a provider to an existing user account does not invalidate the
224+
// refresh token of that account. In other words, the existing account would
225+
// continue to be able to access resources, despite not having used the newly
226+
// linked provider to log in. If you wish to force the user to authenticate
227+
// with this new provider, you need to (a) revoke their refresh token (see
228+
// https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
229+
// and (b) ensure no other authentication methods are present on this account.
230+
func (u *UserToUpdate) ProviderToLink(userProvider *UserProvider) *UserToUpdate {
231+
return u.set("linkProviderUserInfo", userProvider)
232+
}
233+
234+
// ProvidersToDelete unlinks this user from the specified providers.
235+
func (u *UserToUpdate) ProvidersToDelete(providerIds []string) *UserToUpdate {
236+
// skip setting the value to empty if it's already empty.
237+
if len(providerIds) == 0 {
238+
if u.params == nil {
239+
return u
240+
}
241+
if _, ok := u.params["providersToDelete"]; !ok {
242+
return u
243+
}
244+
}
245+
246+
return u.set("providersToDelete", providerIds)
247+
}
248+
220249
// revokeRefreshTokens revokes all refresh tokens for a user by setting the validSince property
221250
// to the present in epoch seconds.
222251
func (u *UserToUpdate) revokeRefreshTokens() *UserToUpdate {
@@ -296,6 +325,78 @@ func (u *UserToUpdate) validatedRequest() (map[string]interface{}, error) {
296325
return nil, err
297326
}
298327
}
328+
329+
if linkProviderUserInfo, ok := req["linkProviderUserInfo"]; ok {
330+
userProvider := linkProviderUserInfo.(*UserProvider)
331+
if err := validateProviderUserInfo(userProvider); err != nil {
332+
return nil, err
333+
}
334+
335+
// Although we don't really advertise it, we want to also handle linking of
336+
// non-federated idps with this call. So if we detect one of them, we'll
337+
// adjust the properties parameter appropriately. This *does* imply that a
338+
// conflict could arise, e.g. if the user provides a phoneNumber property,
339+
// but also provides a providerToLink with a 'phone' provider id. In that
340+
// case, we'll return an error.
341+
342+
if userProvider.ProviderID == "email" {
343+
if _, ok := req["email"]; ok {
344+
// We could relax this to only return an error if the email addrs don't
345+
// match. But for now, we'll be extra picky.
346+
return nil, errors.New(
347+
"both UserToUpdate.Email and UserToUpdate.ProviderToLink.ProviderID='email' " +
348+
"were set; to link to the email/password provider, only specify the " +
349+
"UserToUpdate.Email field")
350+
}
351+
req["email"] = userProvider.UID
352+
delete(req, "linkProviderUserInfo")
353+
} else if userProvider.ProviderID == "phone" {
354+
if _, ok := req["phoneNumber"]; ok {
355+
// We could relax this to only return an error if the phone numbers don't
356+
// match. But for now, we'll be extra picky.
357+
return nil, errors.New(
358+
"both UserToUpdate.PhoneNumber and UserToUpdate.ProviderToLink.ProviderID='phone' " +
359+
"were set; to link to the phone provider, only specify the " +
360+
"UserToUpdate.PhoneNumber field")
361+
}
362+
req["phoneNumber"] = userProvider.UID
363+
delete(req, "linkProviderUserInfo")
364+
}
365+
}
366+
367+
if providersToDelete, ok := req["providersToDelete"]; ok {
368+
var deleteProvider []string
369+
list, ok := req["deleteProvider"]
370+
if ok {
371+
deleteProvider = list.([]string)
372+
}
373+
374+
for _, providerToDelete := range providersToDelete.([]string) {
375+
if providerToDelete == "" {
376+
return nil, errors.New("providersToDelete must not include empty strings")
377+
}
378+
379+
// If we've been told to unlink the phone provider both via setting
380+
// phoneNumber to "" *and* by setting providersToDelete to include
381+
// 'phone', then we'll reject that. Though it might also be reasonable to
382+
// relax this restriction and just unlink it.
383+
if providerToDelete == "phone" {
384+
for _, prov := range deleteProvider {
385+
if prov == "phone" {
386+
return nil, errors.New("both UserToUpdate.PhoneNumber='' and " +
387+
"UserToUpdate.ProvidersToDelete=['phone'] were set; to unlink from a " +
388+
"phone provider, only specify the UserToUpdate.PhoneNumber='' field")
389+
}
390+
}
391+
}
392+
393+
deleteProvider = append(deleteProvider, providerToDelete)
394+
}
395+
396+
req["deleteProvider"] = deleteProvider
397+
delete(req, "providersToDelete")
398+
}
399+
299400
return req, nil
300401
}
301402

@@ -455,6 +556,16 @@ func validatePhone(phone string) error {
455556
return nil
456557
}
457558

559+
func validateProviderUserInfo(p *UserProvider) error {
560+
if p.UID == "" {
561+
return fmt.Errorf("user provider must specify a uid")
562+
}
563+
if p.ProviderID == "" {
564+
return fmt.Errorf("user provider must specify a provider ID")
565+
}
566+
return nil
567+
}
568+
458569
func validateProvider(providerID string, providerUID string) error {
459570
if providerID == "" {
460571
return fmt.Errorf("providerID must be a non-empty string")

auth/user_mgt_test.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,48 @@ func TestInvalidUpdateUser(t *testing.T) {
654654
}, {
655655
(&UserToUpdate{}).Password("short"),
656656
"password must be a string at least 6 characters long",
657+
}, {
658+
(&UserToUpdate{}).ProviderToLink(&UserProvider{UID: "google_uid"}),
659+
"user provider must specify a provider ID",
660+
}, {
661+
(&UserToUpdate{}).ProviderToLink(&UserProvider{ProviderID: "google.com"}),
662+
"user provider must specify a uid",
663+
}, {
664+
(&UserToUpdate{}).ProviderToLink(&UserProvider{ProviderID: "google.com", UID: ""}),
665+
"user provider must specify a uid",
666+
}, {
667+
(&UserToUpdate{}).ProviderToLink(&UserProvider{ProviderID: "", UID: "google_uid"}),
668+
"user provider must specify a provider ID",
669+
}, {
670+
(&UserToUpdate{}).ProvidersToDelete([]string{""}),
671+
"providersToDelete must not include empty strings",
672+
}, {
673+
(&UserToUpdate{}).
674+
Email("user@example.com").
675+
ProviderToLink(&UserProvider{
676+
ProviderID: "email",
677+
UID: "user@example.com",
678+
}),
679+
"both UserToUpdate.Email and UserToUpdate.ProviderToLink.ProviderID='email' " +
680+
"were set; to link to the email/password provider, only specify the " +
681+
"UserToUpdate.Email field",
682+
}, {
683+
(&UserToUpdate{}).
684+
PhoneNumber("+15555550001").
685+
ProviderToLink(&UserProvider{
686+
ProviderID: "phone",
687+
UID: "+15555550001",
688+
}),
689+
"both UserToUpdate.PhoneNumber and UserToUpdate.ProviderToLink.ProviderID='phone' " +
690+
"were set; to link to the phone provider, only specify the " +
691+
"UserToUpdate.PhoneNumber field",
692+
}, {
693+
(&UserToUpdate{}).
694+
PhoneNumber("").
695+
ProvidersToDelete([]string{"phone"}),
696+
"both UserToUpdate.PhoneNumber='' and " +
697+
"UserToUpdate.ProvidersToDelete=['phone'] were set; to unlink from a " +
698+
"phone provider, only specify the UserToUpdate.PhoneNumber='' field",
657699
},
658700
}
659701

@@ -752,6 +794,43 @@ var updateUserCases = []struct {
752794
"deleteProvider": []string{"phone"},
753795
},
754796
},
797+
{
798+
(&UserToUpdate{}).ProviderToLink(&UserProvider{
799+
ProviderID: "google.com",
800+
UID: "google_uid",
801+
}),
802+
map[string]interface{}{
803+
"linkProviderUserInfo": &UserProvider{
804+
ProviderID: "google.com",
805+
UID: "google_uid",
806+
}},
807+
},
808+
{
809+
(&UserToUpdate{}).PhoneNumber("").ProvidersToDelete([]string{"google.com"}),
810+
map[string]interface{}{
811+
"deleteProvider": []string{"phone", "google.com"},
812+
},
813+
},
814+
{
815+
(&UserToUpdate{}).ProvidersToDelete([]string{"email", "phone"}),
816+
map[string]interface{}{
817+
"deleteProvider": []string{"email", "phone"},
818+
},
819+
},
820+
{
821+
(&UserToUpdate{}).ProviderToLink(&UserProvider{
822+
ProviderID: "email",
823+
UID: "user@example.com",
824+
}),
825+
map[string]interface{}{"email": "user@example.com"},
826+
},
827+
{
828+
(&UserToUpdate{}).ProviderToLink(&UserProvider{
829+
ProviderID: "phone",
830+
UID: "+15555550001",
831+
}),
832+
map[string]interface{}{"phoneNumber": "+15555550001"},
833+
},
755834
{
756835
(&UserToUpdate{}).CustomClaims(map[string]interface{}{"a": strings.Repeat("a", 992)}),
757836
map[string]interface{}{"customAttributes": fmt.Sprintf(`{"a":%q}`, strings.Repeat("a", 992))},
@@ -1115,7 +1194,7 @@ func TestUserToImportError(t *testing.T) {
11151194
ProviderID: "google.com",
11161195
},
11171196
}),
1118-
"user provdier must specify a uid",
1197+
"user provider must specify a uid",
11191198
},
11201199
}
11211200

0 commit comments

Comments
 (0)