Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion auth/tenant_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func TestTenantListUsers(t *testing.T) {
want := []*ExportedUserRecord{
{UserRecord: testUser, PasswordHash: "passwordhash1", PasswordSalt: "salt1"},
{UserRecord: testUser, PasswordHash: "passwordhash2", PasswordSalt: "salt2"},
{UserRecord: testUser, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
{UserRecord: testUserWithoutMFA, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
}

testIterator := func(iter *UserIterator, token string, req string) {
Expand Down
92 changes: 75 additions & 17 deletions auth/user_mgt.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ type UserInfo struct {
UID string `json:"rawId,omitempty"`
}

// multiFactorInfoResponse describes the `mfaInfo` of the user record API response
type multiFactorInfoResponse struct {
MFAEnrollmentID string `json:"mfaEnrollmentId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
PhoneInfo string `json:"phoneInfo,omitempty"`
EnrolledAt string `json:"enrolledAt,omitempty"`
}

// MultiFactorID represents the type of an enrolled factor, for now only Phone
// is available.
type MultiFactorID string

const (
// Phone represents an enrolled factor of type Phone / SMS
Phone MultiFactorID = "phone"
)

// MultiFactorInfo describes a user enrolled second phone factor.
type MultiFactorInfo struct {
UID string
DisplayName string
EnrollmentTimestamp int64
FactorID MultiFactorID
PhoneNumber string
}

// MultiFactorSettings describes the multi-factor related user settings.
type MultiFactorSettings struct {
EnrolledFactors []*MultiFactorInfo
}

// UserMetadata contains additional metadata associated with a user account.
// Timestamps are in milliseconds since epoch.
type UserMetadata struct {
Expand All @@ -77,6 +108,7 @@ type UserRecord struct {
TokensValidAfterMillis int64 // milliseconds since epoch.
UserMetadata *UserMetadata
TenantID string
MultiFactor *MultiFactorSettings
}

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

type userQueryResponse struct {
UID string `json:"localId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Email string `json:"email,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
LastRefreshAt string `json:"lastRefreshAt,omitempty"`
ProviderID string `json:"providerId,omitempty"`
CustomAttributes string `json:"customAttributes,omitempty"`
Disabled bool `json:"disabled,omitempty"`
EmailVerified bool `json:"emailVerified,omitempty"`
ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"`
PasswordHash string `json:"passwordHash,omitempty"`
PasswordSalt string `json:"salt,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ValidSinceSeconds int64 `json:"validSince,string,omitempty"`
UID string `json:"localId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Email string `json:"email,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
CreationTimestamp int64 `json:"createdAt,string,omitempty"`
LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"`
LastRefreshAt string `json:"lastRefreshAt,omitempty"`
ProviderID string `json:"providerId,omitempty"`
CustomAttributes string `json:"customAttributes,omitempty"`
Disabled bool `json:"disabled,omitempty"`
EmailVerified bool `json:"emailVerified,omitempty"`
ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"`
PasswordHash string `json:"passwordHash,omitempty"`
PasswordSalt string `json:"salt,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ValidSinceSeconds int64 `json:"validSince,string,omitempty"`
MFAInfo []*multiFactorInfoResponse `json:"mfaInfo,omitempty"`
}

func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) {
Expand Down Expand Up @@ -948,6 +981,28 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
lastRefreshTimestamp = t.Unix() * 1000
}

// Map the MFA info to a slice of enrolled factors. Currently there is only
// support for PhoneMultiFactorInfo.
var enrolledFactors []*MultiFactorInfo
for _, factor := range r.MFAInfo {
var enrollmentTimestamp int64
if factor.EnrolledAt != "" {
t, err := time.Parse(time.RFC3339, factor.EnrolledAt)
if err != nil {
return nil, err
}
enrollmentTimestamp = t.Unix() * 1000
}

enrolledFactors = append(enrolledFactors, &MultiFactorInfo{
UID: factor.MFAEnrollmentID,
DisplayName: factor.DisplayName,
EnrollmentTimestamp: enrollmentTimestamp,
FactorID: Phone,
PhoneNumber: factor.PhoneInfo,
})
}

return &ExportedUserRecord{
UserRecord: &UserRecord{
UserInfo: &UserInfo{
Expand All @@ -969,6 +1024,9 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
CreationTimestamp: r.CreationTimestamp,
LastRefreshTimestamp: lastRefreshTimestamp,
},
MultiFactor: &MultiFactorSettings{
EnrolledFactors: enrolledFactors,
},
},
PasswordHash: hash,
PasswordSalt: r.PasswordSalt,
Expand Down
59 changes: 58 additions & 1 deletion auth/user_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,55 @@ var testUser = &UserRecord{
},
CustomClaims: map[string]interface{}{"admin": true, "package": "gold"},
TenantID: "testTenant",
MultiFactor: &MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
FactorID: Phone,
EnrollmentTimestamp: 1614776780000,
PhoneNumber: "+1234567890",
DisplayName: "My MFA Phone",
},
},
},
}

var emptyFactors []*MultiFactorInfo
var testUserWithoutMFA = &UserRecord{
UserInfo: &UserInfo{
UID: "testusernomfa",
Email: "[email protected]",
PhoneNumber: "+1234567890",
DisplayName: "Test User Without MFA",
PhotoURL: "http://www.example.com/testusernomfa/photo.png",
ProviderID: defaultProviderID,
},
Disabled: false,

EmailVerified: true,
ProviderUserInfo: []*UserInfo{
{
ProviderID: "password",
DisplayName: "Test User Without MFA",
PhotoURL: "http://www.example.com/testusernomfa/photo.png",
Email: "[email protected]",
UID: "testuid",
}, {
ProviderID: "phone",
PhoneNumber: "+1234567890",
UID: "testuid",
},
},
TokensValidAfterMillis: 1494364393000,
UserMetadata: &UserMetadata{
CreationTimestamp: 1234567890000,
LastLogInTimestamp: 1233211232000,
},
CustomClaims: map[string]interface{}{"admin": true, "package": "gold"},
TenantID: "testTenant",
MultiFactor: &MultiFactorSettings{
EnrolledFactors: emptyFactors,
},
}

func TestGetUser(t *testing.T) {
Expand Down Expand Up @@ -501,7 +550,7 @@ func TestListUsers(t *testing.T) {
want := []*ExportedUserRecord{
{UserRecord: testUser, PasswordHash: "passwordhash1", PasswordSalt: "salt1"},
{UserRecord: testUser, PasswordHash: "passwordhash2", PasswordSalt: "salt2"},
{UserRecord: testUser, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
{UserRecord: testUserWithoutMFA, PasswordHash: "passwordhash3", PasswordSalt: "salt3"},
}

testIterator := func(iter *UserIterator, token string, req string) {
Expand Down Expand Up @@ -1596,6 +1645,14 @@ func TestMakeExportedUser(t *testing.T) {
PhoneNumber: "+1234567890",
UID: "testuid",
}},
MFAInfo: []*multiFactorInfoResponse{
{
PhoneInfo: "+1234567890",
MFAEnrollmentID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
DisplayName: "My MFA Phone",
EnrolledAt: "2021-03-03T13:06:20.542896Z",
},
},
}

want := &ExportedUserRecord{
Expand Down
10 changes: 9 additions & 1 deletion testdata/get_user.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@
"createdAt": "1234567890000",
"lastLoginAt": "1233211232000",
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
"tenantId": "testTenant"
"tenantId": "testTenant",
"mfaInfo": [
{
"phoneInfo": "+1234567890",
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
"displayName": "My MFA Phone",
"enrolledAt": "2021-03-03T13:06:20.542896Z"
}
]
}
]
}
36 changes: 26 additions & 10 deletions testdata/list_users.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@
"createdAt": "1234567890000",
"lastLoginAt": "1233211232000",
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
"tenantId": "testTenant"
"tenantId": "testTenant",
"mfaInfo": [
{
"phoneInfo": "+1234567890",
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
"displayName": "My MFA Phone",
"enrolledAt": "2021-03-03T13:06:20.542896Z"
}
]
},
{
"localId": "testuser",
Expand Down Expand Up @@ -63,21 +71,29 @@
"createdAt": "1234567890000",
"lastLoginAt": "1233211232000",
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
"tenantId": "testTenant"
"tenantId": "testTenant",
"mfaInfo": [
{
"phoneInfo": "+1234567890",
"mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be",
"displayName": "My MFA Phone",
"enrolledAt": "2021-03-03T13:06:20.542896Z"
}
]
},
{
"localId": "testuser",
"email": "testuser@example.com",
"localId": "testusernomfa",
"email": "testusernomfa@example.com",
"phoneNumber": "+1234567890",
"emailVerified": true,
"displayName": "Test User",
"displayName": "Test User Without MFA",
"providerUserInfo": [
{
"providerId": "password",
"displayName": "Test User",
"photoUrl": "http://www.example.com/testuser/photo.png",
"federatedId": "testuser@example.com",
"email": "testuser@example.com",
"displayName": "Test User Without MFA",
"photoUrl": "http://www.example.com/testusernomfa/photo.png",
"federatedId": "testusernomfa@example.com",
"email": "testusernomfa@example.com",
"rawId": "testuid"
},
{
Expand All @@ -86,7 +102,7 @@
"rawId": "testuid"
}
],
"photoUrl": "http://www.example.com/testuser/photo.png",
"photoUrl": "http://www.example.com/testusernomfa/photo.png",
"passwordHash": "passwordhash3",
"salt": "salt3",
"passwordUpdatedAt": 1.494364393E+12,
Expand Down