@@ -34,6 +34,12 @@ const (
3434 maxLenPayloadCC = 1000
3535 defaultProviderID = "firebase"
3636 idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1"
37+
38+ // Maximum number of users allowed to batch get at a time.
39+ maxGetAccountsBatchSize = 100
40+
41+ // Maximum number of users allowed to batch delete at a time.
42+ maxDeleteAccountsBatchSize = 1000
3743)
3844
3945// 'REDACTED', encoded as a base64 string.
@@ -57,6 +63,9 @@ type UserInfo struct {
5763type UserMetadata struct {
5864 CreationTimestamp int64
5965 LastLogInTimestamp int64
66+ // The time at which the user was last active (ID token refreshed), or 0 if
67+ // the user was never active.
68+ LastRefreshTimestamp int64
6069}
6170
6271// UserRecord contains metadata associated with a Firebase user account.
@@ -491,6 +500,15 @@ func validatePhone(phone string) error {
491500 return nil
492501}
493502
503+ func validateProvider (providerID string , providerUID string ) error {
504+ if providerID == "" {
505+ return fmt .Errorf ("providerID must be a non-empty string" )
506+ } else if providerUID == "" {
507+ return fmt .Errorf ("providerUID must be a non-empty string" )
508+ }
509+ return nil
510+ }
511+
494512// End of validators
495513
496514// GetUser gets the user data corresponding to the specified user ID.
@@ -545,12 +563,13 @@ func (q *userQuery) build() map[string]interface{} {
545563 }
546564}
547565
566+ type getAccountInfoResponse struct {
567+ Users []* userQueryResponse `json:"users"`
568+ }
569+
548570func (c * baseClient ) getUser (ctx context.Context , query * userQuery ) (* UserRecord , error ) {
549- var parsed struct {
550- Users []* userQueryResponse `json:"users"`
551- }
552- _ , err := c .post (ctx , "/accounts:lookup" , query .build (), & parsed )
553- if err != nil {
571+ var parsed getAccountInfoResponse
572+ if _ , err := c .post (ctx , "/accounts:lookup" , query .build (), & parsed ); err != nil {
554573 return nil , err
555574 }
556575
@@ -561,6 +580,195 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord
561580 return parsed .Users [0 ].makeUserRecord ()
562581}
563582
583+ // A UserIdentifier identifies a user to be looked up.
584+ type UserIdentifier interface {
585+ matches (ur * UserRecord ) bool
586+ populate (req * getAccountInfoRequest )
587+ }
588+
589+ // A UIDIdentifier is used for looking up an account by uid.
590+ //
591+ // See GetUsers function.
592+ type UIDIdentifier struct {
593+ UID string
594+ }
595+
596+ func (id UIDIdentifier ) matches (ur * UserRecord ) bool {
597+ return id .UID == ur .UID
598+ }
599+
600+ func (id UIDIdentifier ) populate (req * getAccountInfoRequest ) {
601+ req .LocalID = append (req .LocalID , id .UID )
602+ }
603+
604+ // An EmailIdentifier is used for looking up an account by email.
605+ //
606+ // See GetUsers function.
607+ type EmailIdentifier struct {
608+ Email string
609+ }
610+
611+ func (id EmailIdentifier ) matches (ur * UserRecord ) bool {
612+ return id .Email == ur .Email
613+ }
614+
615+ func (id EmailIdentifier ) populate (req * getAccountInfoRequest ) {
616+ req .Email = append (req .Email , id .Email )
617+ }
618+
619+ // A PhoneIdentifier is used for looking up an account by phone number.
620+ //
621+ // See GetUsers function.
622+ type PhoneIdentifier struct {
623+ PhoneNumber string
624+ }
625+
626+ func (id PhoneIdentifier ) matches (ur * UserRecord ) bool {
627+ return id .PhoneNumber == ur .PhoneNumber
628+ }
629+
630+ func (id PhoneIdentifier ) populate (req * getAccountInfoRequest ) {
631+ req .PhoneNumber = append (req .PhoneNumber , id .PhoneNumber )
632+ }
633+
634+ // A ProviderIdentifier is used for looking up an account by federated provider.
635+ //
636+ // See GetUsers function.
637+ type ProviderIdentifier struct {
638+ ProviderID string
639+ ProviderUID string
640+ }
641+
642+ func (id ProviderIdentifier ) matches (ur * UserRecord ) bool {
643+ for _ , userInfo := range ur .ProviderUserInfo {
644+ if id .ProviderID == userInfo .ProviderID && id .ProviderUID == userInfo .UID {
645+ return true
646+ }
647+ }
648+ return false
649+ }
650+
651+ func (id ProviderIdentifier ) populate (req * getAccountInfoRequest ) {
652+ req .FederatedUserID = append (
653+ req .FederatedUserID ,
654+ federatedUserIdentifier {ProviderID : id .ProviderID , RawID : id .ProviderUID })
655+ }
656+
657+ // A GetUsersResult represents the result of the GetUsers() API.
658+ type GetUsersResult struct {
659+ // Set of UserRecords corresponding to the set of users that were requested.
660+ // Only users that were found are listed here. The result set is unordered.
661+ Users []* UserRecord
662+
663+ // Set of UserIdentifiers that were requested, but not found.
664+ NotFound []UserIdentifier
665+ }
666+
667+ type federatedUserIdentifier struct {
668+ ProviderID string `json:"providerId,omitempty"`
669+ RawID string `json:"rawId,omitempty"`
670+ }
671+
672+ type getAccountInfoRequest struct {
673+ LocalID []string `json:"localId,omitempty"`
674+ Email []string `json:"email,omitempty"`
675+ PhoneNumber []string `json:"phoneNumber,omitempty"`
676+ FederatedUserID []federatedUserIdentifier `json:"federatedUserId,omitempty"`
677+ }
678+
679+ func (req * getAccountInfoRequest ) validate () error {
680+ for i := range req .LocalID {
681+ if err := validateUID (req .LocalID [i ]); err != nil {
682+ return err
683+ }
684+ }
685+
686+ for i := range req .Email {
687+ if err := validateEmail (req .Email [i ]); err != nil {
688+ return err
689+ }
690+ }
691+
692+ for i := range req .PhoneNumber {
693+ if err := validatePhone (req .PhoneNumber [i ]); err != nil {
694+ return err
695+ }
696+ }
697+
698+ for i := range req .FederatedUserID {
699+ id := & req .FederatedUserID [i ]
700+ if err := validateProvider (id .ProviderID , id .RawID ); err != nil {
701+ return err
702+ }
703+ }
704+
705+ return nil
706+ }
707+
708+ func isUserFound (id UserIdentifier , urs [](* UserRecord )) bool {
709+ for i := range urs {
710+ if id .matches (urs [i ]) {
711+ return true
712+ }
713+ }
714+ return false
715+ }
716+
717+ // GetUsers returns the user data corresponding to the specified identifiers.
718+ //
719+ // There are no ordering guarantees; in particular, the nth entry in the users
720+ // result list is not guaranteed to correspond to the nth entry in the input
721+ // parameters list.
722+ //
723+ // A maximum of 100 identifiers may be supplied. If more than 100
724+ // identifiers are supplied, this method returns an error.
725+ //
726+ // Returns the corresponding user records. An error is returned instead if any
727+ // of the identifiers are invalid or if more than 100 identifiers are
728+ // specified.
729+ func (c * baseClient ) GetUsers (
730+ ctx context.Context , identifiers []UserIdentifier ,
731+ ) (* GetUsersResult , error ) {
732+ if len (identifiers ) == 0 {
733+ return & GetUsersResult {[](* UserRecord ){}, [](UserIdentifier ){}}, nil
734+ } else if len (identifiers ) > maxGetAccountsBatchSize {
735+ return nil , fmt .Errorf (
736+ "`identifiers` parameter must have <= %d entries" , maxGetAccountsBatchSize )
737+ }
738+
739+ var request getAccountInfoRequest
740+ for i := range identifiers {
741+ identifiers [i ].populate (& request )
742+ }
743+
744+ if err := request .validate (); err != nil {
745+ return nil , err
746+ }
747+
748+ var parsed getAccountInfoResponse
749+ if _ , err := c .post (ctx , "/accounts:lookup" , request , & parsed ); err != nil {
750+ return nil , err
751+ }
752+
753+ var userRecords [](* UserRecord )
754+ for _ , user := range parsed .Users {
755+ userRecord , err := user .makeUserRecord ()
756+ if err != nil {
757+ return nil , err
758+ }
759+ userRecords = append (userRecords , userRecord )
760+ }
761+
762+ var notFound []UserIdentifier
763+ for i := range identifiers {
764+ if ! isUserFound (identifiers [i ], userRecords ) {
765+ notFound = append (notFound , identifiers [i ])
766+ }
767+ }
768+
769+ return & GetUsersResult {userRecords , notFound }, nil
770+ }
771+
564772type userQueryResponse struct {
565773 UID string `json:"localId,omitempty"`
566774 DisplayName string `json:"displayName,omitempty"`
@@ -569,6 +777,7 @@ type userQueryResponse struct {
569777 PhotoURL string `json:"photoUrl,omitempty"`
570778 CreationTimestamp int64 `json:"createdAt,string,omitempty"`
571779 LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
780+ LastRefreshAt string `json:"lastRefreshAt,omitempty"`
572781 ProviderID string `json:"providerId,omitempty"`
573782 CustomAttributes string `json:"customAttributes,omitempty"`
574783 Disabled bool `json:"disabled,omitempty"`
@@ -592,8 +801,7 @@ func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) {
592801func (r * userQueryResponse ) makeExportedUserRecord () (* ExportedUserRecord , error ) {
593802 var customClaims map [string ]interface {}
594803 if r .CustomAttributes != "" {
595- err := json .Unmarshal ([]byte (r .CustomAttributes ), & customClaims )
596- if err != nil {
804+ if err := json .Unmarshal ([]byte (r .CustomAttributes ), & customClaims ); err != nil {
597805 return nil , err
598806 }
599807 if len (customClaims ) == 0 {
@@ -609,6 +817,15 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
609817 hash = ""
610818 }
611819
820+ var lastRefreshTimestamp int64
821+ if r .LastRefreshAt != "" {
822+ t , err := time .Parse (time .RFC3339 , r .LastRefreshAt )
823+ if err != nil {
824+ return nil , err
825+ }
826+ lastRefreshTimestamp = t .Unix () * 1000
827+ }
828+
612829 return & ExportedUserRecord {
613830 UserRecord : & UserRecord {
614831 UserInfo : & UserInfo {
@@ -626,8 +843,9 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
626843 TenantID : r .TenantID ,
627844 TokensValidAfterMillis : r .ValidSinceSeconds * 1000 ,
628845 UserMetadata : & UserMetadata {
629- LastLogInTimestamp : r .LastLogInTimestamp ,
630- CreationTimestamp : r .CreationTimestamp ,
846+ LastLogInTimestamp : r .LastLogInTimestamp ,
847+ CreationTimestamp : r .CreationTimestamp ,
848+ LastRefreshTimestamp : lastRefreshTimestamp ,
631849 },
632850 },
633851 PasswordHash : hash ,
@@ -728,6 +946,91 @@ func (c *baseClient) DeleteUser(ctx context.Context, uid string) error {
728946 return err
729947}
730948
949+ // A DeleteUsersResult represents the result of the DeleteUsers() call.
950+ type DeleteUsersResult struct {
951+ // The number of users that were deleted successfully (possibly zero). Users
952+ // that did not exist prior to calling DeleteUsers() are considered to be
953+ // successfully deleted.
954+ SuccessCount int
955+
956+ // The number of users that failed to be deleted (possibly zero).
957+ FailureCount int
958+
959+ // A list of DeleteUsersErrorInfo instances describing the errors that were
960+ // encountered during the deletion. Length of this list is equal to the value
961+ // of FailureCount.
962+ Errors []* DeleteUsersErrorInfo
963+ }
964+
965+ // DeleteUsersErrorInfo represents an error encountered while deleting a user
966+ // account.
967+ //
968+ // The Index field corresponds to the index of the failed user in the uids
969+ // array that was passed to DeleteUsers().
970+ type DeleteUsersErrorInfo struct {
971+ Index int `json:"index,omitEmpty"`
972+ Reason string `json:"message,omitEmpty"`
973+ }
974+
975+ // DeleteUsers deletes the users specified by the given identifiers.
976+ //
977+ // Deleting a non-existing user won't generate an error. (i.e. this method is
978+ // idempotent.) Non-existing users are considered to be successfully
979+ // deleted, and are therefore counted in the DeleteUsersResult.SuccessCount
980+ // value.
981+ //
982+ // A maximum of 1000 identifiers may be supplied. If more than 1000
983+ // identifiers are supplied, this method returns an error.
984+ //
985+ // This API is currently rate limited at the server to 1 QPS. If you exceed
986+ // this, you may get a quota exceeded error. Therefore, if you want to delete
987+ // more than 1000 users, you may need to add a delay to ensure you don't go
988+ // over this limit.
989+ //
990+ // Returns the total number of successful/failed deletions, as well as the
991+ // array of errors that correspond to the failed deletions. An error is
992+ // returned if any of the identifiers are invalid or if more than 1000
993+ // identifiers are specified.
994+ func (c * baseClient ) DeleteUsers (ctx context.Context , uids []string ) (* DeleteUsersResult , error ) {
995+ if len (uids ) == 0 {
996+ return & DeleteUsersResult {}, nil
997+ } else if len (uids ) > maxDeleteAccountsBatchSize {
998+ return nil , fmt .Errorf (
999+ "`uids` parameter must have <= %d entries" , maxDeleteAccountsBatchSize )
1000+ }
1001+
1002+ var payload struct {
1003+ LocalIds []string `json:"localIds"`
1004+ Force bool `json:"force"`
1005+ }
1006+ payload .Force = true
1007+
1008+ for i := range uids {
1009+ if err := validateUID (uids [i ]); err != nil {
1010+ return nil , err
1011+ }
1012+
1013+ payload .LocalIds = append (payload .LocalIds , uids [i ])
1014+ }
1015+
1016+ type batchDeleteAccountsResponse struct {
1017+ Errors []* DeleteUsersErrorInfo `json:"errors"`
1018+ }
1019+
1020+ resp := batchDeleteAccountsResponse {}
1021+ if _ , err := c .post (ctx , "/accounts:batchDelete" , payload , & resp ); err != nil {
1022+ return nil , err
1023+ }
1024+
1025+ result := DeleteUsersResult {
1026+ FailureCount : len (resp .Errors ),
1027+ SuccessCount : len (uids ) - len (resp .Errors ),
1028+ Errors : resp .Errors ,
1029+ }
1030+
1031+ return & result , nil
1032+ }
1033+
7311034// SessionCookie creates a new Firebase session cookie from the given ID token and expiry
7321035// duration. The returned JWT can be set as a server-side session cookie with a custom cookie
7331036// policy. Expiry duration must be at least 5 minutes but may not exceed 14 days.
0 commit comments