Skip to content

Commit 3782eb6

Browse files
authored
Implemented GetUser* APIs with internal.HTTPClient (#235)
* Decoupling GetUser() API from ID toolkit client * Removing some redundant code * Reworked GetUserByEmail() and GetUserByPhoneNumber() * Minor reordering of code * Turned userQuery into a struct type
1 parent 1f56b03 commit 3782eb6

File tree

2 files changed

+150
-67
lines changed

2 files changed

+150
-67
lines changed

auth/user_mgt.go

Lines changed: 146 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ func (c *Client) setHeader(ic identitytoolkitCall) {
4646

4747
// UserInfo is a collection of standard profile information for a user.
4848
type UserInfo struct {
49-
DisplayName string
50-
Email string
51-
PhoneNumber string
52-
PhotoURL string
49+
DisplayName string `json:"displayName,omitempty"`
50+
Email string `json:"email,omitempty"`
51+
PhoneNumber string `json:"phoneNumber,omitempty"`
52+
PhotoURL string `json:"photoUrl,omitempty"`
5353
// In the ProviderUserInfo[] ProviderID can be a short domain name (e.g. google.com),
5454
// or the identity of an OpenID identity provider.
5555
// In UserRecord.UserInfo it will return the constant string "firebase".
56-
ProviderID string
57-
UID string
56+
ProviderID string `json:"providerId,omitempty"`
57+
UID string `json:"rawId,omitempty"`
5858
}
5959

6060
// UserMetadata contains additional metadata associated with a user account.
@@ -350,39 +350,6 @@ func (c *Client) DeleteUser(ctx context.Context, uid string) error {
350350
return nil
351351
}
352352

353-
// GetUser gets the user data corresponding to the specified user ID.
354-
func (c *Client) GetUser(ctx context.Context, uid string) (*UserRecord, error) {
355-
if err := validateUID(uid); err != nil {
356-
return nil, err
357-
}
358-
request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{
359-
LocalId: []string{uid},
360-
}
361-
return c.getUser(ctx, request)
362-
}
363-
364-
// GetUserByPhoneNumber gets the user data corresponding to the specified user phone number.
365-
func (c *Client) GetUserByPhoneNumber(ctx context.Context, phone string) (*UserRecord, error) {
366-
if err := validatePhone(phone); err != nil {
367-
return nil, err
368-
}
369-
request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{
370-
PhoneNumber: []string{phone},
371-
}
372-
return c.getUser(ctx, request)
373-
}
374-
375-
// GetUserByEmail gets the user data corresponding to the specified email.
376-
func (c *Client) GetUserByEmail(ctx context.Context, email string) (*UserRecord, error) {
377-
if err := validateEmail(email); err != nil {
378-
return nil, err
379-
}
380-
request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{
381-
Email: []string{email},
382-
}
383-
return c.getUser(ctx, request)
384-
}
385-
386353
// RevokeRefreshTokens revokes all refresh tokens issued to a user.
387354
//
388355
// RevokeRefreshTokens updates the user's TokensValidAfterMillis to the current UTC second.
@@ -611,32 +578,6 @@ func (c *Client) updateUser(ctx context.Context, uid string, user *UserToUpdate)
611578
return nil
612579
}
613580

614-
func (c *Client) getUser(ctx context.Context, request *identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest) (*UserRecord, error) {
615-
call := c.is.Relyingparty.GetAccountInfo(request)
616-
c.setHeader(call)
617-
resp, err := call.Context(ctx).Do()
618-
if err != nil {
619-
return nil, handleServerError(err)
620-
}
621-
if len(resp.Users) == 0 {
622-
var msg string
623-
if len(request.LocalId) == 1 {
624-
msg = fmt.Sprintf("cannot find user from uid: %q", request.LocalId[0])
625-
} else if len(request.Email) == 1 {
626-
msg = fmt.Sprintf("cannot find user from email: %q", request.Email[0])
627-
} else {
628-
msg = fmt.Sprintf("cannot find user from phone number: %q", request.PhoneNumber[0])
629-
}
630-
return nil, internal.Error(userNotFound, msg)
631-
}
632-
633-
eu, err := makeExportedUser(resp.Users[0])
634-
if err != nil {
635-
return nil, err
636-
}
637-
return eu.UserRecord, nil
638-
}
639-
640581
const idToolkitEndpoint = "https://identitytoolkit.googleapis.com/v1/projects"
641582

642583
// userManagementClient is a helper for interacting with the Identity Toolkit REST API.
@@ -652,6 +593,146 @@ type userManagementClient struct {
652593
httpClient *internal.HTTPClient
653594
}
654595

596+
// GetUser gets the user data corresponding to the specified user ID.
597+
func (c *userManagementClient) GetUser(ctx context.Context, uid string) (*UserRecord, error) {
598+
return c.getUser(ctx, &userQuery{
599+
field: "localId",
600+
value: uid,
601+
label: "uid",
602+
})
603+
}
604+
605+
// GetUserByEmail gets the user data corresponding to the specified email.
606+
func (c *userManagementClient) GetUserByEmail(ctx context.Context, email string) (*UserRecord, error) {
607+
if err := validateEmail(email); err != nil {
608+
return nil, err
609+
}
610+
return c.getUser(ctx, &userQuery{
611+
field: "email",
612+
value: email,
613+
})
614+
}
615+
616+
// GetUserByPhoneNumber gets the user data corresponding to the specified user phone number.
617+
func (c *userManagementClient) GetUserByPhoneNumber(ctx context.Context, phone string) (*UserRecord, error) {
618+
if err := validatePhone(phone); err != nil {
619+
return nil, err
620+
}
621+
return c.getUser(ctx, &userQuery{
622+
field: "phoneNumber",
623+
value: phone,
624+
label: "phone number",
625+
})
626+
}
627+
628+
type userQuery struct {
629+
field string
630+
value string
631+
label string
632+
}
633+
634+
func (q *userQuery) description() string {
635+
label := q.label
636+
if label == "" {
637+
label = q.field
638+
}
639+
return fmt.Sprintf("%s: %q", label, q.value)
640+
}
641+
642+
func (q *userQuery) build() map[string]interface{} {
643+
return map[string]interface{}{
644+
q.field: []string{q.value},
645+
}
646+
}
647+
648+
func (c *userManagementClient) getUser(ctx context.Context, query *userQuery) (*UserRecord, error) {
649+
resp, err := c.post(ctx, "/accounts:lookup", query.build())
650+
if err != nil {
651+
return nil, err
652+
}
653+
654+
if resp.Status != http.StatusOK {
655+
return nil, handleHTTPError(resp)
656+
}
657+
658+
var parsed struct {
659+
Users []*userQueryResponse `json:"users"`
660+
}
661+
if err := json.Unmarshal(resp.Body, &parsed); err != nil {
662+
return nil, err
663+
}
664+
665+
if len(parsed.Users) == 0 {
666+
return nil, internal.Errorf(userNotFound, "cannot find user from %s", query.description())
667+
}
668+
669+
return parsed.Users[0].makeUserRecord()
670+
}
671+
672+
type userQueryResponse struct {
673+
UID string `json:"localId,omitempty"`
674+
DisplayName string `json:"displayName,omitempty"`
675+
Email string `json:"email,omitempty"`
676+
PhoneNumber string `json:"phoneNumber,omitempty"`
677+
PhotoURL string `json:"photoUrl,omitempty"`
678+
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
679+
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
680+
ProviderID string `json:"providerId,omitempty"`
681+
CustomAttributes string `json:"customAttributes,omitempty"`
682+
Disabled bool `json:"disabled,omitempty"`
683+
EmailVerified bool `json:"emailVerified,omitempty"`
684+
ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"`
685+
PasswordHash string `json:"passwordHash,omitempty"`
686+
PasswordSalt string `json:"salt,omitempty"`
687+
ValidSinceSeconds int64 `json:"validSince,string,omitempty"`
688+
}
689+
690+
func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) {
691+
exported, err := r.makeExportedUserRecord()
692+
if err != nil {
693+
return nil, err
694+
}
695+
696+
return exported.UserRecord, nil
697+
}
698+
699+
func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error) {
700+
var customClaims map[string]interface{}
701+
if r.CustomAttributes != "" {
702+
err := json.Unmarshal([]byte(r.CustomAttributes), &customClaims)
703+
if err != nil {
704+
return nil, err
705+
}
706+
if len(customClaims) == 0 {
707+
customClaims = nil
708+
}
709+
}
710+
711+
return &ExportedUserRecord{
712+
UserRecord: &UserRecord{
713+
UserInfo: &UserInfo{
714+
DisplayName: r.DisplayName,
715+
Email: r.Email,
716+
PhoneNumber: r.PhoneNumber,
717+
PhotoURL: r.PhotoURL,
718+
UID: r.UID,
719+
ProviderID: defaultProviderID,
720+
},
721+
CustomClaims: customClaims,
722+
Disabled: r.Disabled,
723+
EmailVerified: r.EmailVerified,
724+
ProviderUserInfo: r.ProviderUserInfo,
725+
TokensValidAfterMillis: r.ValidSinceSeconds * 1000,
726+
UserMetadata: &UserMetadata{
727+
LastLogInTimestamp: r.LastLogInTimestamp,
728+
CreationTimestamp: r.CreationTimestamp,
729+
},
730+
},
731+
PasswordHash: r.PasswordHash,
732+
PasswordSalt: r.PasswordSalt,
733+
}, nil
734+
}
735+
655736
// SessionCookie creates a new Firebase session cookie from the given ID token and expiry
656737
// duration. The returned JWT can be set as a server-side session cookie with a custom cookie
657738
// policy. Expiry duration must be at least 5 minutes but may not exceed 14 days.

auth/user_mgt_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,14 +1143,15 @@ func TestSessionCookieLongExpiresIn(t *testing.T) {
11431143
func TestHTTPError(t *testing.T) {
11441144
s := echoServer([]byte(`{"error":"test"}`), t)
11451145
defer s.Close()
1146+
s.Client.httpClient.RetryConfig = nil
11461147
s.Status = http.StatusInternalServerError
11471148

11481149
u, err := s.Client.GetUser(context.Background(), "some uid")
11491150
if u != nil || err == nil {
11501151
t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err)
11511152
}
11521153

1153-
want := `googleapi: got HTTP response code 500 with body: {"error":"test"}`
1154+
want := `http error status: 500; body: {"error":"test"}`
11541155
if err.Error() != want || !IsUnknown(err) {
11551156
t.Errorf("GetUser() = %v; want = %q", err, want)
11561157
}
@@ -1168,6 +1169,7 @@ func TestHTTPErrorWithCode(t *testing.T) {
11681169
}
11691170
s := echoServer(nil, t)
11701171
defer s.Close()
1172+
s.Client.httpClient.RetryConfig = nil
11711173
s.Status = http.StatusInternalServerError
11721174

11731175
for code, check := range errorCodes {
@@ -1177,7 +1179,7 @@ func TestHTTPErrorWithCode(t *testing.T) {
11771179
t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err)
11781180
}
11791181

1180-
want := fmt.Sprintf("googleapi: Error 500: %s", code)
1182+
want := fmt.Sprintf(`http error status: 500; body: {"error":{"message":"%s"}}`, code)
11811183
if err.Error() != want || !check(err) {
11821184
t.Errorf("GetUser() = %v; want = %q", err, want)
11831185
}

0 commit comments

Comments
 (0)