Skip to content

Commit

Permalink
feat(auth): Add TotpInfo field to UserRecord (#573)
Browse files Browse the repository at this point in the history
* `PhoneMultiFactorConfig` instead of `PhoneNumber`

* Undo messaging_batch changes

* deprecated comment for `PhoneNumber` field

* Add tests for `PhoneNumber` integration check

* Keeping `PhoneNumber` in fetched `UserInfo` to avoid breaking changes

* updated user settings

* Update variable names

* TW feedback changes

* remove duplicate changes

* fix tests failed due to merging

* remove overriding statement
  • Loading branch information
pragatimodi authored Nov 7, 2023
1 parent 96101fd commit 8355839
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 66 deletions.
93 changes: 69 additions & 24 deletions auth/user_mgt.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
createUserMethod = "createUser"
updateUserMethod = "updateUser"
phoneMultiFactorID = "phone"
totpMultiFactorID = "totp"
)

// 'REDACTED', encoded as a base64 string.
Expand All @@ -62,24 +63,37 @@ type UserInfo struct {

// 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"`
MFAEnrollmentID string `json:"mfaEnrollmentId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
PhoneInfo string `json:"phoneInfo,omitempty"`
TOTPInfo *TOTPInfo `json:"totpInfo,omitempty"`
EnrolledAt string `json:"enrolledAt,omitempty"`
}

// TOTPInfo describes a user enrolled second TOTP factor.
type TOTPInfo struct{}

// PhoneMultiFactorInfo describes a user enrolled in SMS second factor.
type PhoneMultiFactorInfo struct {
PhoneNumber string
}

// TOTPMultiFactorInfo describes a user enrolled in TOTP second factor.
type TOTPMultiFactorInfo struct{}

type multiFactorEnrollments struct {
Enrollments []*multiFactorInfoResponse `json:"enrollments"`
}

// MultiFactorInfo describes a user enrolled second phone factor.
// TODO : convert PhoneNumber to PhoneMultiFactorInfo struct
type MultiFactorInfo struct {
UID string
DisplayName string
EnrollmentTimestamp int64
FactorID string
PhoneNumber string
PhoneNumber string // Deprecated: Use PhoneMultiFactorInfo instead
Phone *PhoneMultiFactorInfo
TOTP *TOTPMultiFactorInfo
}

// MultiFactorSettings describes the multi-factor related user settings.
Expand Down Expand Up @@ -177,12 +191,17 @@ func convertMultiFactorInfoToServerFormat(mfaInfo MultiFactorInfo) (multiFactorI
if mfaInfo.UID != "" {
authFactorInfo.MFAEnrollmentID = mfaInfo.UID
}
if mfaInfo.FactorID == phoneMultiFactorID {
authFactorInfo.PhoneInfo = mfaInfo.PhoneNumber
return authFactorInfo, nil

switch mfaInfo.FactorID {
case phoneMultiFactorID:
authFactorInfo.PhoneInfo = mfaInfo.Phone.PhoneNumber
case totpMultiFactorID:
authFactorInfo.TOTPInfo = (*TOTPInfo)(mfaInfo.TOTP)
default:
out, _ := json.Marshal(mfaInfo)
return multiFactorInfoResponse{}, fmt.Errorf("unsupported second factor %s provided", string(out))
}
out, _ := json.Marshal(mfaInfo)
return multiFactorInfoResponse{}, fmt.Errorf("unsupported second factor %s provided", string(out))
return authFactorInfo, nil
}

func (u *UserToCreate) validatedRequest() (map[string]interface{}, error) {
Expand Down Expand Up @@ -338,8 +357,7 @@ func (u *UserToUpdate) validatedRequest() (map[string]interface{}, error) {
if err != nil {
return nil, err
}

// https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/update
// Request body ref: https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/update
req["mfa"] = multiFactorEnrollments{mfaInfo}
} else {
req[k] = v
Expand Down Expand Up @@ -679,8 +697,24 @@ func validateAndFormatMfaSettings(mfaSettings MultiFactorSettings, methodType st
return nil, fmt.Errorf("the second factor \"displayName\" for \"%s\" must be a valid non-empty string", multiFactorInfo.DisplayName)
}
if multiFactorInfo.FactorID == phoneMultiFactorID {
if err := validatePhone(multiFactorInfo.PhoneNumber); err != nil {
return nil, fmt.Errorf("the second factor \"phoneNumber\" for \"%s\" must be a non-empty E.164 standard compliant identifier string", multiFactorInfo.PhoneNumber)
if multiFactorInfo.Phone != nil {
// If PhoneMultiFactorInfo is provided, validate its PhoneNumber field
if err := validatePhone(multiFactorInfo.Phone.PhoneNumber); err != nil {
return nil, fmt.Errorf("the second factor \"phoneNumber\" for \"%s\" must be a non-empty E.164 standard compliant identifier string", multiFactorInfo.Phone.PhoneNumber)
}
// No need for the else here since we are returning from the function
} else if multiFactorInfo.PhoneNumber != "" {
// PhoneMultiFactorInfo is nil, check the deprecated PhoneNumber field
if err := validatePhone(multiFactorInfo.PhoneNumber); err != nil {
return nil, fmt.Errorf("the second factor \"phoneNumber\" for \"%s\" must be a non-empty E.164 standard compliant identifier string", multiFactorInfo.PhoneNumber)
}
// The PhoneNumber field is deprecated, set it in PhoneMultiFactorInfo and inform about the deprecation.
multiFactorInfo.Phone = &PhoneMultiFactorInfo{
PhoneNumber: multiFactorInfo.PhoneNumber,
}
} else {
// Both PhoneMultiFactorInfo and deprecated PhoneNumber are missing.
return nil, fmt.Errorf("\"PhoneMultiFactorInfo\" must be defined")
}
}
obj, err := convertMultiFactorInfoToServerFormat(*multiFactorInfo)
Expand Down Expand Up @@ -1079,17 +1113,28 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
enrollmentTimestamp = t.Unix() * 1000
}

if factor.PhoneInfo == "" {
if factor.PhoneInfo != "" {
enrolledFactors = append(enrolledFactors, &MultiFactorInfo{
UID: factor.MFAEnrollmentID,
DisplayName: factor.DisplayName,
EnrollmentTimestamp: enrollmentTimestamp,
FactorID: phoneMultiFactorID,
PhoneNumber: factor.PhoneInfo,
Phone: &PhoneMultiFactorInfo{
PhoneNumber: factor.PhoneInfo,
},
})
} else if factor.TOTPInfo != nil {
enrolledFactors = append(enrolledFactors, &MultiFactorInfo{
UID: factor.MFAEnrollmentID,
DisplayName: factor.DisplayName,
EnrollmentTimestamp: enrollmentTimestamp,
FactorID: totpMultiFactorID,
TOTP: &TOTPMultiFactorInfo{},
})
} else {
return nil, fmt.Errorf("unsupported multi-factor auth response: %#v", factor)
}

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

return &ExportedUserRecord{
Expand Down
99 changes: 76 additions & 23 deletions auth/user_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,21 @@ var testUser = &UserRecord{
MultiFactor: &MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
UID: "enrolledPhoneFactor",
FactorID: "phone",
EnrollmentTimestamp: 1614776780000,
PhoneNumber: "+1234567890",
DisplayName: "My MFA Phone",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+1234567890",
},
PhoneNumber: "+1234567890",
DisplayName: "My MFA Phone",
},
{
UID: "enrolledTOTPFactor",
FactorID: "totp",
EnrollmentTimestamp: 1614776780000,
TOTP: &TOTPMultiFactorInfo{},
DisplayName: "My MFA TOTP",
},
},
},
Expand Down Expand Up @@ -646,8 +656,10 @@ func TestInvalidCreateUser(t *testing.T) {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "EnrollmentID",
PhoneNumber: "+11234567890",
UID: "EnrollmentID",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "Spouse's phone number",
FactorID: "phone",
},
Expand All @@ -658,7 +670,9 @@ func TestInvalidCreateUser(t *testing.T) {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
PhoneNumber: "invalid",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "invalid",
},
DisplayName: "Spouse's phone number",
FactorID: "phone",
},
Expand All @@ -669,7 +683,9 @@ func TestInvalidCreateUser(t *testing.T) {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
PhoneNumber: "+11234567890",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "Spouse's phone number",
FactorID: "phone",
EnrollmentTimestamp: time.Now().UTC().Unix(),
Expand All @@ -681,7 +697,9 @@ func TestInvalidCreateUser(t *testing.T) {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
PhoneNumber: "+11234567890",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "Spouse's phone number",
FactorID: "",
},
Expand All @@ -692,8 +710,10 @@ func TestInvalidCreateUser(t *testing.T) {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
PhoneNumber: "+11234567890",
FactorID: "phone",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
FactorID: "phone",
},
},
}),
Expand Down Expand Up @@ -772,30 +792,45 @@ var createUserCases = []struct {
}, {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "Phone Number active",
FactorID: "phone",
},
{
PhoneNumber: "+11234567890",
DisplayName: "Spouse's phone number",
DisplayName: "Phone Number deprecated",
FactorID: "phone",
},
},
}),
map[string]interface{}{"mfaInfo": []*multiFactorInfoResponse{
{
PhoneInfo: "+11234567890",
DisplayName: "Spouse's phone number",
DisplayName: "Phone Number active",
},
{
PhoneInfo: "+11234567890",
DisplayName: "Phone Number deprecated",
},
},
},
}, {
(&UserToCreate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
PhoneNumber: "+11234567890",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "number1",
FactorID: "phone",
},
{
PhoneNumber: "+11234567890",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "number2",
FactorID: "phone",
},
Expand Down Expand Up @@ -875,9 +910,11 @@ func TestInvalidUpdateUser(t *testing.T) {
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "enrolledSecondFactor1",
PhoneNumber: "+11234567890",
FactorID: "phone",
UID: "enrolledSecondFactor1",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
FactorID: "phone",
},
},
}),
Expand All @@ -886,8 +923,10 @@ func TestInvalidUpdateUser(t *testing.T) {
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "enrolledSecondFactor1",
PhoneNumber: "invalid",
UID: "enrolledSecondFactor1",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "invalid",
},
DisplayName: "Spouse's phone number",
FactorID: "phone",
},
Expand Down Expand Up @@ -1038,17 +1077,25 @@ var updateUserCases = []struct {
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
EnrolledFactors: []*MultiFactorInfo{
{
UID: "enrolledSecondFactor1",
PhoneNumber: "+11234567890",
UID: "enrolledSecondFactor1",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
DisplayName: "Spouse's phone number",
FactorID: "phone",
EnrollmentTimestamp: time.Now().Unix(),
}, {
UID: "enrolledSecondFactor2",
UID: "enrolledSecondFactor2",
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
PhoneNumber: "+11234567890",
DisplayName: "Spouse's phone number",
FactorID: "phone",
}, {
Phone: &PhoneMultiFactorInfo{
PhoneNumber: "+11234567890",
},
PhoneNumber: "+11234567890",
DisplayName: "Spouse's phone number",
FactorID: "phone",
Expand Down Expand Up @@ -1883,10 +1930,16 @@ func TestMakeExportedUser(t *testing.T) {
MFAInfo: []*multiFactorInfoResponse{
{
PhoneInfo: "+1234567890",
MFAEnrollmentID: "0aaded3f-5e73-461d-aef9-37b48e3769be",
MFAEnrollmentID: "enrolledPhoneFactor",
DisplayName: "My MFA Phone",
EnrolledAt: "2021-03-03T13:06:20.542896Z",
},
{
TOTPInfo: &TOTPInfo{},
MFAEnrollmentID: "enrolledTOTPFactor",
DisplayName: "My MFA TOTP",
EnrolledAt: "2021-03-03T13:06:20.542896Z",
},
},
}

Expand Down
Loading

0 comments on commit 8355839

Please sign in to comment.