Skip to content

Commit 84aa056

Browse files
authored
feat(auth): MFA Support for GoLang createUser, updateUser (#511)
* Golang MFA support MFA Support for golang - draft * Some changes * Fixing unit tests * Fixing lint issues * Error fix : missing go.sum entry for module providing package golang.org/x/lint/golint * Resolved comments * Added an integration test to create a user with non-null MFA values and addressing comments * Addressing comments
1 parent d6b0ca5 commit 84aa056

File tree

4 files changed

+324
-4
lines changed

4 files changed

+324
-4
lines changed

auth/user_mgt.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const (
3939

4040
// Maximum number of users allowed to batch delete at a time.
4141
maxDeleteAccountsBatchSize = 1000
42+
createUserMethod = "createUser"
43+
updateUserMethod = "updateUser"
44+
phoneMultiFactorID = "phone"
4245
)
4346

4447
// 'REDACTED', encoded as a base64 string.
@@ -66,6 +69,7 @@ type multiFactorInfoResponse struct {
6669
}
6770

6871
// MultiFactorInfo describes a user enrolled second phone factor.
72+
// TODO : convert PhoneNumber to PhoneMultiFactorInfo struct
6973
type MultiFactorInfo struct {
7074
UID string
7175
DisplayName string
@@ -147,6 +151,11 @@ func (u *UserToCreate) UID(uid string) *UserToCreate {
147151
return u.set("localId", uid)
148152
}
149153

154+
// MFASettings setter.
155+
func (u *UserToCreate) MFASettings(mfaSettings MultiFactorSettings) *UserToCreate {
156+
return u.set("mfaSettings", mfaSettings)
157+
}
158+
150159
func (u *UserToCreate) set(key string, value interface{}) *UserToCreate {
151160
if u.params == nil {
152161
u.params = make(map[string]interface{})
@@ -155,10 +164,34 @@ func (u *UserToCreate) set(key string, value interface{}) *UserToCreate {
155164
return u
156165
}
157166

167+
// Converts a client format second factor object to server format.
168+
func convertMultiFactorInfoToServerFormat(mfaInfo MultiFactorInfo) (multiFactorInfoResponse, error) {
169+
var authFactorInfo multiFactorInfoResponse
170+
if mfaInfo.EnrollmentTimestamp != 0 {
171+
authFactorInfo.EnrolledAt = time.Unix(mfaInfo.EnrollmentTimestamp, 0).Format("2006-01-02T15:04:05Z07:00Z")
172+
}
173+
if mfaInfo.FactorID == phoneMultiFactorID {
174+
authFactorInfo.PhoneInfo = mfaInfo.PhoneNumber
175+
authFactorInfo.DisplayName = mfaInfo.DisplayName
176+
authFactorInfo.MFAEnrollmentID = mfaInfo.UID
177+
return authFactorInfo, nil
178+
}
179+
out, _ := json.Marshal(mfaInfo)
180+
return multiFactorInfoResponse{}, fmt.Errorf("Unsupported second factor %s provided", string(out))
181+
}
182+
158183
func (u *UserToCreate) validatedRequest() (map[string]interface{}, error) {
159184
req := make(map[string]interface{})
160185
for k, v := range u.params {
161-
req[k] = v
186+
if k == "mfaSettings" {
187+
mfaInfo, err := validateAndFormatMfaSettings(v.(MultiFactorSettings), createUserMethod)
188+
if err != nil {
189+
return nil, err
190+
}
191+
req["mfaInfo"] = mfaInfo
192+
} else {
193+
req[k] = v
194+
}
162195
}
163196

164197
if uid, ok := req["localId"]; ok {
@@ -191,7 +224,6 @@ func (u *UserToCreate) validatedRequest() (map[string]interface{}, error) {
191224
return nil, err
192225
}
193226
}
194-
195227
return req, nil
196228
}
197229

@@ -241,6 +273,11 @@ func (u *UserToUpdate) PhotoURL(url string) *UserToUpdate {
241273
return u.set("photoUrl", url)
242274
}
243275

276+
// MFASettings setter.
277+
func (u *UserToUpdate) MFASettings(mfaSettings MultiFactorSettings) *UserToUpdate {
278+
return u.set("mfaSettings", mfaSettings)
279+
}
280+
244281
// ProviderToLink links this user to the specified provider.
245282
//
246283
// Linking a provider to an existing user account does not invalidate the
@@ -291,7 +328,15 @@ func (u *UserToUpdate) validatedRequest() (map[string]interface{}, error) {
291328

292329
req := make(map[string]interface{})
293330
for k, v := range u.params {
294-
req[k] = v
331+
if k == "mfaSettings" {
332+
mfaInfo, err := validateAndFormatMfaSettings(v.(MultiFactorSettings), updateUserMethod)
333+
if err != nil {
334+
return nil, err
335+
}
336+
req["mfaInfo"] = mfaInfo
337+
} else {
338+
req[k] = v
339+
}
295340
}
296341

297342
if email, ok := req["email"]; ok {
@@ -604,6 +649,45 @@ func validateProvider(providerID string, providerUID string) error {
604649
return nil
605650
}
606651

652+
func validateAndFormatMfaSettings(mfaSettings MultiFactorSettings, methodType string) ([]*multiFactorInfoResponse, error) {
653+
var mfaInfo []*multiFactorInfoResponse
654+
for _, multiFactorInfo := range mfaSettings.EnrolledFactors {
655+
if multiFactorInfo.FactorID == "" {
656+
return nil, fmt.Errorf("no factor id specified")
657+
}
658+
switch methodType {
659+
case createUserMethod:
660+
// Enrollment time and uid are not allowed for signupNewUser endpoint. They will automatically be provisioned server side.
661+
if multiFactorInfo.EnrollmentTimestamp != 0 {
662+
return nil, fmt.Errorf("\"EnrollmentTimeStamp\" is not supported when adding second factors via \"createUser()\"")
663+
}
664+
if multiFactorInfo.UID != "" {
665+
return nil, fmt.Errorf("\"uid\" is not supported when adding second factors via \"createUser()\"")
666+
}
667+
case updateUserMethod:
668+
if multiFactorInfo.UID == "" {
669+
return nil, fmt.Errorf("the second factor \"uid\" must be a valid non-empty string when adding second factors via \"updateUser()\"")
670+
}
671+
default:
672+
return nil, fmt.Errorf("unsupported methodType: %s", methodType)
673+
}
674+
if err := validateDisplayName(multiFactorInfo.DisplayName); err != nil {
675+
return nil, fmt.Errorf("the second factor \"displayName\" for \"%s\" must be a valid non-empty string", multiFactorInfo.DisplayName)
676+
}
677+
if multiFactorInfo.FactorID == phoneMultiFactorID {
678+
if err := validatePhone(multiFactorInfo.PhoneNumber); err != nil {
679+
return nil, fmt.Errorf("the second factor \"phoneNumber\" for \"%s\" must be a non-empty E.164 standard compliant identifier string", multiFactorInfo.PhoneNumber)
680+
}
681+
}
682+
obj, err := convertMultiFactorInfoToServerFormat(*multiFactorInfo)
683+
if err != nil {
684+
return nil, err
685+
}
686+
mfaInfo = append(mfaInfo, &obj)
687+
}
688+
return mfaInfo, nil
689+
}
690+
607691
// End of validators
608692

609693
// GetUser gets the user data corresponding to the specified user ID.
@@ -999,7 +1083,7 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error
9991083
UID: factor.MFAEnrollmentID,
10001084
DisplayName: factor.DisplayName,
10011085
EnrollmentTimestamp: enrollmentTimestamp,
1002-
FactorID: "phone",
1086+
FactorID: phoneMultiFactorID,
10031087
PhoneNumber: factor.PhoneInfo,
10041088
})
10051089
}

auth/user_mgt_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,62 @@ func TestInvalidCreateUser(t *testing.T) {
642642
}, {
643643
(&UserToCreate{}).Email("a@a@a"),
644644
`malformed email string: "a@a@a"`,
645+
}, {
646+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
647+
EnrolledFactors: []*MultiFactorInfo{
648+
{
649+
UID: "EnrollmentID",
650+
PhoneNumber: "+11234567890",
651+
DisplayName: "Spouse's phone number",
652+
FactorID: "phone",
653+
},
654+
},
655+
}),
656+
`"uid" is not supported when adding second factors via "createUser()"`,
657+
}, {
658+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
659+
EnrolledFactors: []*MultiFactorInfo{
660+
{
661+
PhoneNumber: "invalid",
662+
DisplayName: "Spouse's phone number",
663+
FactorID: "phone",
664+
},
665+
},
666+
}),
667+
`the second factor "phoneNumber" for "invalid" must be a non-empty E.164 standard compliant identifier string`,
668+
}, {
669+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
670+
EnrolledFactors: []*MultiFactorInfo{
671+
{
672+
PhoneNumber: "+11234567890",
673+
DisplayName: "Spouse's phone number",
674+
FactorID: "phone",
675+
EnrollmentTimestamp: time.Now().UTC().Unix(),
676+
},
677+
},
678+
}),
679+
`"EnrollmentTimeStamp" is not supported when adding second factors via "createUser()"`,
680+
}, {
681+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
682+
EnrolledFactors: []*MultiFactorInfo{
683+
{
684+
PhoneNumber: "+11234567890",
685+
DisplayName: "Spouse's phone number",
686+
FactorID: "",
687+
},
688+
},
689+
}),
690+
`no factor id specified`,
691+
}, {
692+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
693+
EnrolledFactors: []*MultiFactorInfo{
694+
{
695+
PhoneNumber: "+11234567890",
696+
FactorID: "phone",
697+
},
698+
},
699+
}),
700+
`the second factor "displayName" for "" must be a valid non-empty string`,
645701
},
646702
}
647703
client := &Client{
@@ -713,6 +769,49 @@ var createUserCases = []struct {
713769
{
714770
(&UserToCreate{}).PhotoURL("http://some.url"),
715771
map[string]interface{}{"photoUrl": "http://some.url"},
772+
}, {
773+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
774+
EnrolledFactors: []*MultiFactorInfo{
775+
{
776+
PhoneNumber: "+11234567890",
777+
DisplayName: "Spouse's phone number",
778+
FactorID: "phone",
779+
},
780+
},
781+
}),
782+
map[string]interface{}{"mfaInfo": []*multiFactorInfoResponse{
783+
{
784+
PhoneInfo: "+11234567890",
785+
DisplayName: "Spouse's phone number",
786+
},
787+
},
788+
},
789+
}, {
790+
(&UserToCreate{}).MFASettings(MultiFactorSettings{
791+
EnrolledFactors: []*MultiFactorInfo{
792+
{
793+
PhoneNumber: "+11234567890",
794+
DisplayName: "number1",
795+
FactorID: "phone",
796+
},
797+
{
798+
PhoneNumber: "+11234567890",
799+
DisplayName: "number2",
800+
FactorID: "phone",
801+
},
802+
},
803+
}),
804+
map[string]interface{}{"mfaInfo": []*multiFactorInfoResponse{
805+
{
806+
PhoneInfo: "+11234567890",
807+
DisplayName: "number1",
808+
},
809+
{
810+
PhoneInfo: "+11234567890",
811+
DisplayName: "number2",
812+
},
813+
},
814+
},
716815
},
717816
}
718817

@@ -772,6 +871,40 @@ func TestInvalidUpdateUser(t *testing.T) {
772871
}, {
773872
(&UserToUpdate{}).Password("short"),
774873
"password must be a string at least 6 characters long",
874+
}, {
875+
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
876+
EnrolledFactors: []*MultiFactorInfo{
877+
{
878+
UID: "enrolledSecondFactor1",
879+
PhoneNumber: "+11234567890",
880+
FactorID: "phone",
881+
},
882+
},
883+
}),
884+
`the second factor "displayName" for "" must be a valid non-empty string`,
885+
}, {
886+
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
887+
EnrolledFactors: []*MultiFactorInfo{
888+
{
889+
UID: "enrolledSecondFactor1",
890+
PhoneNumber: "invalid",
891+
DisplayName: "Spouse's phone number",
892+
FactorID: "phone",
893+
},
894+
},
895+
}),
896+
`the second factor "phoneNumber" for "invalid" must be a non-empty E.164 standard compliant identifier string`,
897+
}, {
898+
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
899+
EnrolledFactors: []*MultiFactorInfo{
900+
{
901+
PhoneNumber: "+11234567890",
902+
FactorID: "phone",
903+
DisplayName: "Spouse's phone number",
904+
},
905+
},
906+
}),
907+
`the second factor "uid" must be a valid non-empty string when adding second factors via "updateUser()"`,
775908
}, {
776909
(&UserToUpdate{}).ProviderToLink(&UserProvider{UID: "google_uid"}),
777910
"user provider must specify a provider ID",
@@ -912,6 +1045,42 @@ var updateUserCases = []struct {
9121045
"deleteProvider": []string{"phone"},
9131046
},
9141047
},
1048+
{
1049+
(&UserToUpdate{}).MFASettings(MultiFactorSettings{
1050+
EnrolledFactors: []*MultiFactorInfo{
1051+
{
1052+
UID: "enrolledSecondFactor1",
1053+
PhoneNumber: "+11234567890",
1054+
DisplayName: "Spouse's phone number",
1055+
FactorID: "phone",
1056+
EnrollmentTimestamp: time.Now().Unix(),
1057+
}, {
1058+
UID: "enrolledSecondFactor2",
1059+
PhoneNumber: "+11234567890",
1060+
DisplayName: "Spouse's phone number",
1061+
FactorID: "phone",
1062+
},
1063+
},
1064+
}),
1065+
map[string]interface{}{"mfaInfo": []*multiFactorInfoResponse{
1066+
{
1067+
MFAEnrollmentID: "enrolledSecondFactor1",
1068+
PhoneInfo: "+11234567890",
1069+
DisplayName: "Spouse's phone number",
1070+
EnrolledAt: time.Now().Format("2006-01-02T15:04:05Z07:00Z"),
1071+
},
1072+
{
1073+
MFAEnrollmentID: "enrolledSecondFactor2",
1074+
DisplayName: "Spouse's phone number",
1075+
PhoneInfo: "+11234567890",
1076+
},
1077+
},
1078+
},
1079+
},
1080+
{
1081+
(&UserToUpdate{}).MFASettings(MultiFactorSettings{}),
1082+
map[string]interface{}{"mfaInfo": nil},
1083+
},
9151084
{
9161085
(&UserToUpdate{}).ProviderToLink(&UserProvider{
9171086
ProviderID: "google.com",

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
250250
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
251251
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
252252
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
253+
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
253254
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
254255
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
255256
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=

0 commit comments

Comments
 (0)