Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Add TotpInfo field to UserRecord #573

Merged
merged 14 commits into from
Nov 7, 2023
Merged
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 != "" {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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")
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
}
}
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,
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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",
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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
Loading