diff --git a/auth/user_mgt.go b/auth/user_mgt.go index fe6ac986..96efd545 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -42,6 +42,7 @@ const ( createUserMethod = "createUser" updateUserMethod = "updateUser" phoneMultiFactorID = "phone" + totpMultiFactorID = "totp" ) // 'REDACTED', encoded as a base64 string. @@ -62,20 +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 server side user enrolled second totp factor. +type TOTPInfo struct{} + +// PhoneMultiFactorInfo describes a user enrolled second phone factor. +type PhoneMultiFactorInfo struct { + PhoneNumber string +} + +// TOTPMultiFactorInfo describes a user enrolled second totp 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 + UID string + DisplayName string + EnrollmentTimestamp int64 + FactorID string + PhoneNumber string `Deprecated:"Use PhoneMultiFactorInfo instead"` + PhoneMultiFactorInfo *PhoneMultiFactorInfo + TOTPMultiFactorInfo *TOTPMultiFactorInfo } // MultiFactorSettings describes the multi-factor related user settings. @@ -166,18 +184,25 @@ func (u *UserToCreate) set(key string, value interface{}) *UserToCreate { // Converts a client format second factor object to server format. func convertMultiFactorInfoToServerFormat(mfaInfo MultiFactorInfo) (multiFactorInfoResponse, error) { - var authFactorInfo multiFactorInfoResponse + authFactorInfo := multiFactorInfoResponse{DisplayName: mfaInfo.DisplayName} if mfaInfo.EnrollmentTimestamp != 0 { authFactorInfo.EnrolledAt = time.Unix(mfaInfo.EnrollmentTimestamp, 0).Format("2006-01-02T15:04:05Z07:00Z") } - if mfaInfo.FactorID == phoneMultiFactorID { - authFactorInfo.PhoneInfo = mfaInfo.PhoneNumber - authFactorInfo.DisplayName = mfaInfo.DisplayName + if mfaInfo.UID != "" { authFactorInfo.MFAEnrollmentID = mfaInfo.UID - return authFactorInfo, nil } - out, _ := json.Marshal(mfaInfo) - return multiFactorInfoResponse{}, fmt.Errorf("Unsupported second factor %s provided", string(out)) + authFactorInfo.MFAEnrollmentID = mfaInfo.UID + + switch mfaInfo.FactorID { + case phoneMultiFactorID: + authFactorInfo.PhoneInfo = mfaInfo.PhoneMultiFactorInfo.PhoneNumber + case totpMultiFactorID: + authFactorInfo.TOTPInfo = (*TOTPInfo)(mfaInfo.TOTPMultiFactorInfo) + default: + 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) { @@ -333,7 +358,8 @@ func (u *UserToUpdate) validatedRequest() (map[string]interface{}, error) { if err != nil { return nil, err } - req["mfaInfo"] = mfaInfo + // Request body ref: https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/update + req["mfa"] = multiFactorEnrollments{mfaInfo} } else { req[k] = v } @@ -665,9 +691,6 @@ func validateAndFormatMfaSettings(mfaSettings MultiFactorSettings, methodType st return nil, fmt.Errorf("\"uid\" is not supported when adding second factors via \"createUser()\"") } case updateUserMethod: - if multiFactorInfo.UID == "" { - return nil, fmt.Errorf("the second factor \"uid\" must be a valid non-empty string when adding second factors via \"updateUser()\"") - } default: return nil, fmt.Errorf("unsupported methodType: %s", methodType) } @@ -675,8 +698,25 @@ 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.PhoneMultiFactorInfo != nil { + // If PhoneMultiFactorInfo is provided, validate its PhoneNumber field + if err := validatePhone(multiFactorInfo.PhoneMultiFactorInfo.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.PhoneMultiFactorInfo.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.PhoneMultiFactorInfo = &PhoneMultiFactorInfo{ + PhoneNumber: multiFactorInfo.PhoneNumber, + } + fmt.Println("`PhoneNumber` is deprecated, use `PhoneMultiFactorInfo` instead") + } else { + // Both PhoneMultiFactorInfo and deprecated PhoneNumber are missing. + return nil, fmt.Errorf("\"PhoneMultiFactorInfo\" must be defined") } } obj, err := convertMultiFactorInfoToServerFormat(*multiFactorInfo) @@ -1075,17 +1115,27 @@ 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, + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: factor.PhoneInfo, + }, + }) + } else if factor.TOTPInfo != nil { + enrolledFactors = append(enrolledFactors, &MultiFactorInfo{ + UID: factor.MFAEnrollmentID, + DisplayName: factor.DisplayName, + EnrollmentTimestamp: enrollmentTimestamp, + FactorID: totpMultiFactorID, + TOTPMultiFactorInfo: &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{ diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 943810cd..9ad64604 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -69,11 +69,20 @@ 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", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+1234567890", + }, + DisplayName: "My MFA Phone", + }, + { + UID: "enrolledTOTPFactor", + FactorID: "totp", + EnrollmentTimestamp: 1614776780000, + TOTPMultiFactorInfo: &TOTPMultiFactorInfo{}, + DisplayName: "My MFA TOTP", }, }, }, @@ -646,8 +655,10 @@ func TestInvalidCreateUser(t *testing.T) { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - UID: "EnrollmentID", - PhoneNumber: "+11234567890", + UID: "EnrollmentID", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, @@ -658,7 +669,9 @@ func TestInvalidCreateUser(t *testing.T) { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "invalid", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "invalid", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, @@ -669,7 +682,9 @@ func TestInvalidCreateUser(t *testing.T) { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", EnrollmentTimestamp: time.Now().UTC().Unix(), @@ -681,7 +696,9 @@ func TestInvalidCreateUser(t *testing.T) { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "", }, @@ -692,8 +709,10 @@ func TestInvalidCreateUser(t *testing.T) { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "+11234567890", - FactorID: "phone", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, + FactorID: "phone", }, }, }), @@ -773,7 +792,9 @@ var createUserCases = []struct { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, @@ -790,12 +811,16 @@ var createUserCases = []struct { (&UserToCreate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "number1", FactorID: "phone", }, { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "number2", FactorID: "phone", }, @@ -875,9 +900,11 @@ func TestInvalidUpdateUser(t *testing.T) { (&UserToUpdate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - UID: "enrolledSecondFactor1", - PhoneNumber: "+11234567890", - FactorID: "phone", + UID: "enrolledSecondFactor1", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, + FactorID: "phone", }, }, }), @@ -886,25 +913,16 @@ func TestInvalidUpdateUser(t *testing.T) { (&UserToUpdate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - UID: "enrolledSecondFactor1", - PhoneNumber: "invalid", + UID: "enrolledSecondFactor1", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "invalid", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, }, }), `the second factor "phoneNumber" for "invalid" must be a non-empty E.164 standard compliant identifier string`, - }, { - (&UserToUpdate{}).MFASettings(MultiFactorSettings{ - EnrolledFactors: []*MultiFactorInfo{ - { - PhoneNumber: "+11234567890", - FactorID: "phone", - DisplayName: "Spouse's phone number", - }, - }, - }), - `the second factor "uid" must be a valid non-empty string when adding second factors via "updateUser()"`, }, { (&UserToUpdate{}).ProviderToLink(&UserProvider{UID: "google_uid"}), "user provider must specify a provider ID", @@ -1049,20 +1067,30 @@ var updateUserCases = []struct { (&UserToUpdate{}).MFASettings(MultiFactorSettings{ EnrolledFactors: []*MultiFactorInfo{ { - UID: "enrolledSecondFactor1", - PhoneNumber: "+11234567890", + UID: "enrolledSecondFactor1", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", EnrollmentTimestamp: time.Now().Unix(), }, { - UID: "enrolledSecondFactor2", - PhoneNumber: "+11234567890", + UID: "enrolledSecondFactor2", + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, + DisplayName: "Spouse's phone number", + FactorID: "phone", + }, { + PhoneMultiFactorInfo: &PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, }, }), - map[string]interface{}{"mfaInfo": []*multiFactorInfoResponse{ + map[string]interface{}{"mfa": multiFactorEnrollments{Enrollments: []*multiFactorInfoResponse{ { MFAEnrollmentID: "enrolledSecondFactor1", PhoneInfo: "+11234567890", @@ -1074,12 +1102,16 @@ var updateUserCases = []struct { DisplayName: "Spouse's phone number", PhoneInfo: "+11234567890", }, - }, + { + DisplayName: "Spouse's phone number", + PhoneInfo: "+11234567890", + }, + }}, }, }, { (&UserToUpdate{}).MFASettings(MultiFactorSettings{}), - map[string]interface{}{"mfaInfo": nil}, + map[string]interface{}{"mfa": multiFactorEnrollments{Enrollments: nil}}, }, { (&UserToUpdate{}).ProviderToLink(&UserProvider{ @@ -1886,10 +1918,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", + }, }, } diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index c1301f6d..6f43224c 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -434,7 +434,9 @@ func TestCreateUserMFA(t *testing.T) { tc.MFASettings(auth.MultiFactorSettings{ EnrolledFactors: []*auth.MultiFactorInfo{ { - PhoneNumber: "+11234567890", + PhoneMultiFactorInfo: &auth.PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, DisplayName: "Spouse's phone number", FactorID: "phone", }, @@ -447,10 +449,12 @@ func TestCreateUserMFA(t *testing.T) { defer deleteUser(user.UID) var factor []*auth.MultiFactorInfo = []*auth.MultiFactorInfo{ { - UID: user.MultiFactor.EnrolledFactors[0].UID, - DisplayName: "Spouse's phone number", - FactorID: "phone", - PhoneNumber: "+11234567890", + UID: user.MultiFactor.EnrolledFactors[0].UID, + DisplayName: "Spouse's phone number", + FactorID: "phone", + PhoneMultiFactorInfo: &auth.PhoneMultiFactorInfo{ + PhoneNumber: "+11234567890", + }, EnrollmentTimestamp: user.MultiFactor.EnrolledFactors[0].EnrollmentTimestamp, }, } diff --git a/testdata/get_user.json b/testdata/get_user.json index a2cac48b..0bf86f95 100644 --- a/testdata/get_user.json +++ b/testdata/get_user.json @@ -35,9 +35,15 @@ "mfaInfo": [ { "phoneInfo": "+1234567890", - "mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be", + "mfaEnrollmentId": "enrolledPhoneFactor", "displayName": "My MFA Phone", "enrolledAt": "2021-03-03T13:06:20.542896Z" + }, + { + "totpInfo": {}, + "mfaEnrollmentId": "enrolledTOTPFactor", + "displayName": "My MFA TOTP", + "enrolledAt": "2021-03-03T13:06:20.542896Z" } ] } diff --git a/testdata/list_users.json b/testdata/list_users.json index bf94ff49..2b630686 100644 --- a/testdata/list_users.json +++ b/testdata/list_users.json @@ -33,12 +33,18 @@ "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", "tenantId": "testTenant", "mfaInfo": [ - { - "phoneInfo": "+1234567890", - "mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be", - "displayName": "My MFA Phone", - "enrolledAt": "2021-03-03T13:06:20.542896Z" - } + { + "phoneInfo": "+1234567890", + "mfaEnrollmentId": "enrolledPhoneFactor", + "displayName": "My MFA Phone", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + }, + { + "totpInfo": {}, + "mfaEnrollmentId": "enrolledTOTPFactor", + "displayName": "My MFA TOTP", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + } ] }, { @@ -73,12 +79,18 @@ "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", "tenantId": "testTenant", "mfaInfo": [ - { - "phoneInfo": "+1234567890", - "mfaEnrollmentId": "0aaded3f-5e73-461d-aef9-37b48e3769be", - "displayName": "My MFA Phone", - "enrolledAt": "2021-03-03T13:06:20.542896Z" - } + { + "phoneInfo": "+1234567890", + "mfaEnrollmentId": "enrolledPhoneFactor", + "displayName": "My MFA Phone", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + }, + { + "totpInfo": {}, + "mfaEnrollmentId": "enrolledTOTPFactor", + "displayName": "My MFA TOTP", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + } ] }, {