Skip to content

Commit ef8f341

Browse files
feat(auth): Add MFA info to UserRecord (#422)
* Add MFA info to UserRecord * Add parity in auth MFA naming with other admin-sdk libraries * Add comments, satisfy linting * Process MFA review remarks Co-authored-by: Hiranya Jayathilaka <hiranya911@gmail.com>
1 parent 08a1529 commit ef8f341

File tree

5 files changed

+169
-30
lines changed

5 files changed

+169
-30
lines changed

auth/tenant_mgt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func TestTenantListUsers(t *testing.T) {
164164
want := []*ExportedUserRecord{
165165
{UserRecord: testUser, PasswordHash: "passwordhash1", PasswordSalt: "salt1"},
166166
{UserRecord: testUser, PasswordHash: "passwordhash2", PasswordSalt: "salt2"},
167-
{UserRecord: testUser, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
167+
{UserRecord: testUserWithoutMFA, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
168168
}
169169

170170
testIterator := func(iter *UserIterator, token string, req string) {

auth/user_mgt.go

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ type UserInfo struct {
5757
UID string `json:"rawId,omitempty"`
5858
}
5959

60+
// multiFactorInfoResponse describes the `mfaInfo` of the user record API response
61+
type multiFactorInfoResponse struct {
62+
MFAEnrollmentID string `json:"mfaEnrollmentId,omitempty"`
63+
DisplayName string `json:"displayName,omitempty"`
64+
PhoneInfo string `json:"phoneInfo,omitempty"`
65+
EnrolledAt string `json:"enrolledAt,omitempty"`
66+
}
67+
68+
// MultiFactorID represents the type of an enrolled factor, for now only Phone
69+
// is available.
70+
type MultiFactorID string
71+
72+
const (
73+
// Phone represents an enrolled factor of type Phone / SMS
74+
Phone MultiFactorID = "phone"
75+
)
76+
77+
// MultiFactorInfo describes a user enrolled second phone factor.
78+
type MultiFactorInfo struct {
79+
UID string
80+
DisplayName string
81+
EnrollmentTimestamp int64
82+
FactorID MultiFactorID
83+
PhoneNumber string
84+
}
85+
86+
// MultiFactorSettings describes the multi-factor related user settings.
87+
type MultiFactorSettings struct {
88+
EnrolledFactors []*MultiFactorInfo
89+
}
90+
6091
// UserMetadata contains additional metadata associated with a user account.
6192
// Timestamps are in milliseconds since epoch.
6293
type UserMetadata struct {
@@ -77,6 +108,7 @@ type UserRecord struct {
77108
TokensValidAfterMillis int64 // milliseconds since epoch.
78109
UserMetadata *UserMetadata
79110
TenantID string
111+
MultiFactor *MultiFactorSettings
80112
}
81113

82114
// UserToCreate is the parameter struct for the CreateUser function.
@@ -892,23 +924,24 @@ func (c *baseClient) GetUsers(
892924
}
893925

894926
type userQueryResponse struct {
895-
UID string `json:"localId,omitempty"`
896-
DisplayName string `json:"displayName,omitempty"`
897-
Email string `json:"email,omitempty"`
898-
PhoneNumber string `json:"phoneNumber,omitempty"`
899-
PhotoURL string `json:"photoUrl,omitempty"`
900-
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
901-
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
902-
LastRefreshAt string `json:"lastRefreshAt,omitempty"`
903-
ProviderID string `json:"providerId,omitempty"`
904-
CustomAttributes string `json:"customAttributes,omitempty"`
905-
Disabled bool `json:"disabled,omitempty"`
906-
EmailVerified bool `json:"emailVerified,omitempty"`
907-
ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"`
908-
PasswordHash string `json:"passwordHash,omitempty"`
909-
PasswordSalt string `json:"salt,omitempty"`
910-
TenantID string `json:"tenantId,omitempty"`
911-
ValidSinceSeconds int64 `json:"validSince,string,omitempty"`
927+
UID string `json:"localId,omitempty"`
928+
DisplayName string `json:"displayName,omitempty"`
929+
Email string `json:"email,omitempty"`
930+
PhoneNumber string `json:"phoneNumber,omitempty"`
931+
PhotoURL string `json:"photoUrl,omitempty"`
932+
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
933+
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
934+
LastRefreshAt string `json:"lastRefreshAt,omitempty"`
935+
ProviderID string `json:"providerId,omitempty"`
936+
CustomAttributes string `json:"customAttributes,omitempty"`
937+
Disabled bool `json:"disabled,omitempty"`
938+
EmailVerified bool `json:"emailVerified,omitempty"`
939+
ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"`
940+
PasswordHash string `json:"passwordHash,omitempty"`
941+
PasswordSalt string `json:"salt,omitempty"`
942+
TenantID string `json:"tenantId,omitempty"`
943+
ValidSinceSeconds int64 `json:"validSince,string,omitempty"`
944+
MFAInfo []*multiFactorInfoResponse `json:"mfaInfo,omitempty"`
912945
}
913946

914947
func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) {
@@ -948,6 +981,28 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
948981
lastRefreshTimestamp = t.Unix() * 1000
949982
}
950983

984+
// Map the MFA info to a slice of enrolled factors. Currently there is only
985+
// support for PhoneMultiFactorInfo.
986+
var enrolledFactors []*MultiFactorInfo
987+
for _, factor := range r.MFAInfo {
988+
var enrollmentTimestamp int64
989+
if factor.EnrolledAt != "" {
990+
t, err := time.Parse(time.RFC3339, factor.EnrolledAt)
991+
if err != nil {
992+
return nil, err
993+
}
994+
enrollmentTimestamp = t.Unix() * 1000
995+
}
996+
997+
enrolledFactors = append(enrolledFactors, &MultiFactorInfo{
998+
UID: factor.MFAEnrollmentID,
999+
DisplayName: factor.DisplayName,
1000+
EnrollmentTimestamp: enrollmentTimestamp,
1001+
FactorID: Phone,
1002+
PhoneNumber: factor.PhoneInfo,
1003+
})
1004+
}
1005+
9511006
return &ExportedUserRecord{
9521007
UserRecord: &UserRecord{
9531008
UserInfo: &UserInfo{
@@ -969,6 +1024,9 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
9691024
CreationTimestamp: r.CreationTimestamp,
9701025
LastRefreshTimestamp: lastRefreshTimestamp,
9711026
},
1027+
MultiFactor: &MultiFactorSettings{
1028+
EnrolledFactors: enrolledFactors,
1029+
},
9721030
},
9731031
PasswordHash: hash,
9741032
PasswordSalt: r.PasswordSalt,

auth/user_mgt_test.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,55 @@ var testUser = &UserRecord{
6767
},
6868
CustomClaims: map[string]interface{}{"admin": true, "package": "gold"},
6969
TenantID: "testTenant",
70+
MultiFactor: &MultiFactorSettings{
71+
EnrolledFactors: []*MultiFactorInfo{
72+
{
73+
UID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
74+
FactorID: Phone,
75+
EnrollmentTimestamp: 1614776780000,
76+
PhoneNumber: "+1234567890",
77+
DisplayName: "My MFA Phone",
78+
},
79+
},
80+
},
81+
}
82+
83+
var emptyFactors []*MultiFactorInfo
84+
var testUserWithoutMFA = &UserRecord{
85+
UserInfo: &UserInfo{
86+
UID: "testusernomfa",
87+
Email: "testusernomfa@example.com",
88+
PhoneNumber: "+1234567890",
89+
DisplayName: "Test User Without MFA",
90+
PhotoURL: "http://www.example.com/testusernomfa/photo.png",
91+
ProviderID: defaultProviderID,
92+
},
93+
Disabled: false,
94+
95+
EmailVerified: true,
96+
ProviderUserInfo: []*UserInfo{
97+
{
98+
ProviderID: "password",
99+
DisplayName: "Test User Without MFA",
100+
PhotoURL: "http://www.example.com/testusernomfa/photo.png",
101+
Email: "testusernomfa@example.com",
102+
UID: "testuid",
103+
}, {
104+
ProviderID: "phone",
105+
PhoneNumber: "+1234567890",
106+
UID: "testuid",
107+
},
108+
},
109+
TokensValidAfterMillis: 1494364393000,
110+
UserMetadata: &UserMetadata{
111+
CreationTimestamp: 1234567890000,
112+
LastLogInTimestamp: 1233211232000,
113+
},
114+
CustomClaims: map[string]interface{}{"admin": true, "package": "gold"},
115+
TenantID: "testTenant",
116+
MultiFactor: &MultiFactorSettings{
117+
EnrolledFactors: emptyFactors,
118+
},
70119
}
71120

72121
func TestGetUser(t *testing.T) {
@@ -501,7 +550,7 @@ func TestListUsers(t *testing.T) {
501550
want := []*ExportedUserRecord{
502551
{UserRecord: testUser, PasswordHash: "passwordhash1", PasswordSalt: "salt1"},
503552
{UserRecord: testUser, PasswordHash: "passwordhash2", PasswordSalt: "salt2"},
504-
{UserRecord: testUser, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
553+
{UserRecord: testUserWithoutMFA, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
505554
}
506555

507556
testIterator := func(iter *UserIterator, token string, req string) {
@@ -1596,6 +1645,14 @@ func TestMakeExportedUser(t *testing.T) {
15961645
PhoneNumber: "+1234567890",
15971646
UID: "testuid",
15981647
}},
1648+
MFAInfo: []*multiFactorInfoResponse{
1649+
{
1650+
PhoneInfo: "+1234567890",
1651+
MFAEnrollmentID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
1652+
DisplayName: "My MFA Phone",
1653+
EnrolledAt: "2021-03-03T13:06:20.542896Z",
1654+
},
1655+
},
15991656
}
16001657

16011658
want := &ExportedUserRecord{

testdata/get_user.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@
3131
"createdAt": "1234567890000",
3232
"lastLoginAt": "1233211232000",
3333
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
34-
"tenantId": "testTenant"
34+
"tenantId": "testTenant",
35+
"mfaInfo": [
36+
{
37+
"phoneInfo": "+1234567890",
38+
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
39+
"displayName": "My MFA Phone",
40+
"enrolledAt": "2021-03-03T13:06:20.542896Z"
41+
}
42+
]
3543
}
3644
]
3745
}

testdata/list_users.json

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@
3131
"createdAt": "1234567890000",
3232
"lastLoginAt": "1233211232000",
3333
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
34-
"tenantId": "testTenant"
34+
"tenantId": "testTenant",
35+
"mfaInfo": [
36+
{
37+
"phoneInfo": "+1234567890",
38+
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
39+
"displayName": "My MFA Phone",
40+
"enrolledAt": "2021-03-03T13:06:20.542896Z"
41+
}
42+
]
3543
},
3644
{
3745
"localId": "testuser",
@@ -63,21 +71,29 @@
6371
"createdAt": "1234567890000",
6472
"lastLoginAt": "1233211232000",
6573
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
66-
"tenantId": "testTenant"
74+
"tenantId": "testTenant",
75+
"mfaInfo": [
76+
{
77+
"phoneInfo": "+1234567890",
78+
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
79+
"displayName": "My MFA Phone",
80+
"enrolledAt": "2021-03-03T13:06:20.542896Z"
81+
}
82+
]
6783
},
6884
{
69-
"localId": "testuser",
70-
"email": "testuser@example.com",
85+
"localId": "testusernomfa",
86+
"email": "testusernomfa@example.com",
7187
"phoneNumber": "+1234567890",
7288
"emailVerified": true,
73-
"displayName": "Test User",
89+
"displayName": "Test User Without MFA",
7490
"providerUserInfo": [
7591
{
7692
"providerId": "password",
77-
"displayName": "Test User",
78-
"photoUrl": "http://www.example.com/testuser/photo.png",
79-
"federatedId": "testuser@example.com",
80-
"email": "testuser@example.com",
93+
"displayName": "Test User Without MFA",
94+
"photoUrl": "http://www.example.com/testusernomfa/photo.png",
95+
"federatedId": "testusernomfa@example.com",
96+
"email": "testusernomfa@example.com",
8197
"rawId": "testuid"
8298
},
8399
{
@@ -86,7 +102,7 @@
86102
"rawId": "testuid"
87103
}
88104
],
89-
"photoUrl": "http://www.example.com/testuser/photo.png",
105+
"photoUrl": "http://www.example.com/testusernomfa/photo.png",
90106
"passwordHash": "passwordhash3",
91107
"salt": "salt3",
92108
"passwordUpdatedAt": 1.494364393E+12,

0 commit comments

Comments
 (0)