@@ -30,8 +30,9 @@ import (
30
30
)
31
31
32
32
const (
33
- maxLenPayloadCC = 1000
34
- defaultProviderID = "firebase"
33
+ maxLenPayloadCC = 1000
34
+ defaultProviderID = "firebase"
35
+ idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1"
35
36
36
37
// Maximum number of users allowed to batch get at a time.
37
38
maxGetAccountsBatchSize = 100
@@ -217,6 +218,34 @@ func (u *UserToUpdate) PhotoURL(url string) *UserToUpdate {
217
218
return u .set ("photoUrl" , url )
218
219
}
219
220
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
+
220
249
// revokeRefreshTokens revokes all refresh tokens for a user by setting the validSince property
221
250
// to the present in epoch seconds.
222
251
func (u * UserToUpdate ) revokeRefreshTokens () * UserToUpdate {
@@ -296,6 +325,78 @@ func (u *UserToUpdate) validatedRequest() (map[string]interface{}, error) {
296
325
return nil , err
297
326
}
298
327
}
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
+
299
400
return req , nil
300
401
}
301
402
@@ -455,6 +556,16 @@ func validatePhone(phone string) error {
455
556
return nil
456
557
}
457
558
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
+
458
569
func validateProvider (providerID string , providerUID string ) error {
459
570
if providerID == "" {
460
571
return fmt .Errorf ("providerID must be a non-empty string" )
0 commit comments