Skip to content

Commit 691aeed

Browse files
Merge pull request #276 from supertokens/passwordPlolicyOnUpdate
feat: optional password validation in updateEmailOrPassword
2 parents 0d74211 + 1d8e99a commit 691aeed

File tree

15 files changed

+150
-36
lines changed

15 files changed

+150
-36
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
## [0.12.0] - 2023-05-03
11+
- added optional password policy check in `updateEmailOrPassword`
12+
1013
## [0.11.0] - 2023-04-28
1114
- Added missing arguments in `GetUsersNewestFirst` and `GetUsersOldestFirst`
1215

recipe/dashboard/api/userdetails/userPut.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func updateEmailForRecipeId(recipeId string, userId string, email string) (updat
7474
}, nil
7575
}
7676

77-
updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &email, nil)
77+
updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &email, nil, nil)
7878

7979
if err != nil {
8080
return updateEmailResponse{}, err
@@ -113,7 +113,7 @@ func updateEmailForRecipeId(recipeId string, userId string, email string) (updat
113113
}, nil
114114
}
115115

116-
updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &email, nil)
116+
updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &email, nil, nil)
117117

118118
if err != nil {
119119
return updateEmailResponse{}, err

recipe/emailpassword/authFlow_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import (
3535
"github.com/supertokens/supertokens-golang/test/unittesting"
3636
)
3737

38-
//SigninFeature Tests
38+
// SigninFeature Tests
3939
func TestDisablingAPIDefaultSigninDoesNotWork(t *testing.T) {
4040
configValue := supertokens.TypeInput{
4141
Supertokens: &supertokens.ConnectionInfo{
@@ -1234,7 +1234,7 @@ func TestHandlePostSignInFunction(t *testing.T) {
12341234

12351235
}
12361236

1237-
//Signout Feature tests
1237+
// Signout Feature tests
12381238
func TestDefaultSignoutRouteRevokesSession(t *testing.T) {
12391239
customAntiCsrfVal := "VIA_TOKEN"
12401240
configValue := supertokens.TypeInput{
@@ -1474,7 +1474,7 @@ func TestSignoutAPIreturnsTryRefreshTokenAndSignoutShouldReturnOK(t *testing.T)
14741474
assert.Equal(t, "", cookieData2["refreshTokenDomain"])
14751475
}
14761476

1477-
//Signup Feature tests
1477+
// Signup Feature tests
14781478
func TestDisablingAPIDefaultSignUpDoesNotWork(t *testing.T) {
14791479
configValue := supertokens.TypeInput{
14801480
Supertokens: &supertokens.ConnectionInfo{

recipe/emailpassword/ep_userIdMapping_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func TestCreateUserIdMappingUpdateEmailPassword(t *testing.T) {
188188

189189
newEmail := "email@example.com"
190190
newPass := "newpass123"
191-
updateResp, err := UpdateEmailOrPassword(externalUserId, &newEmail, &newPass)
191+
updateResp, err := UpdateEmailOrPassword(externalUserId, &newEmail, &newPass, nil)
192192
assert.NoError(t, err)
193193
assert.NotNil(t, updateResp.OK)
194194

recipe/emailpassword/epmodels/recipeInterface.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type RecipeInterface struct {
2424
GetUserByEmail *func(email string, userContext supertokens.UserContext) (*User, error)
2525
CreateResetPasswordToken *func(userID string, userContext supertokens.UserContext) (CreateResetPasswordTokenResponse, error)
2626
ResetPasswordUsingToken *func(token string, newPassword string, userContext supertokens.UserContext) (ResetPasswordUsingTokenResponse, error)
27-
UpdateEmailOrPassword *func(userId string, email *string, password *string, userContext supertokens.UserContext) (UpdateEmailOrPasswordResponse, error)
27+
UpdateEmailOrPassword *func(userId string, email *string, password *string, applyPasswordPolicy *bool, userContext supertokens.UserContext) (UpdateEmailOrPasswordResponse, error)
2828
}
2929

3030
type SignUpResponse struct {
@@ -56,7 +56,12 @@ type ResetPasswordUsingTokenResponse struct {
5656
}
5757

5858
type UpdateEmailOrPasswordResponse struct {
59-
OK *struct{}
60-
UnknownUserIdError *struct{}
61-
EmailAlreadyExistsError *struct{}
59+
OK *struct{}
60+
UnknownUserIdError *struct{}
61+
EmailAlreadyExistsError *struct{}
62+
PasswordPolicyViolatedError *PasswordPolicyViolatedError
63+
}
64+
65+
type PasswordPolicyViolatedError struct {
66+
FailureReason string
6267
}

recipe/emailpassword/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ func ResetPasswordUsingTokenWithContext(token string, newPassword string, userCo
7474
return (*instance.RecipeImpl.ResetPasswordUsingToken)(token, newPassword, userContext)
7575
}
7676

77-
func UpdateEmailOrPasswordWithContext(userId string, email *string, password *string, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
77+
func UpdateEmailOrPasswordWithContext(userId string, email *string, password *string, applyPasswordPolicy *bool, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
7878
instance, err := GetRecipeInstanceOrThrowError()
7979
if err != nil {
8080
return epmodels.UpdateEmailOrPasswordResponse{}, nil
8181
}
82-
return (*instance.RecipeImpl.UpdateEmailOrPassword)(userId, email, password, userContext)
82+
return (*instance.RecipeImpl.UpdateEmailOrPassword)(userId, email, password, applyPasswordPolicy, userContext)
8383
}
8484

8585
func SendEmailWithContext(input emaildelivery.EmailType, userContext supertokens.UserContext) error {
@@ -114,8 +114,8 @@ func ResetPasswordUsingToken(token string, newPassword string) (epmodels.ResetPa
114114
return ResetPasswordUsingTokenWithContext(token, newPassword, &map[string]interface{}{})
115115
}
116116

117-
func UpdateEmailOrPassword(userId string, email *string, password *string) (epmodels.UpdateEmailOrPasswordResponse, error) {
118-
return UpdateEmailOrPasswordWithContext(userId, email, password, &map[string]interface{}{})
117+
func UpdateEmailOrPassword(userId string, email *string, password *string, applyPasswordPolicy *bool) (epmodels.UpdateEmailOrPasswordResponse, error) {
118+
return UpdateEmailOrPasswordWithContext(userId, email, password, applyPasswordPolicy, &map[string]interface{}{})
119119
}
120120

121121
func SendEmail(input emaildelivery.EmailType) error {

recipe/emailpassword/recipe.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ func MakeRecipe(recipeId string, appInfo supertokens.NormalisedAppinfo, config *
5353
verifiedConfig := validateAndNormaliseUserInput(r, appInfo, config)
5454
r.Config = verifiedConfig
5555
r.APIImpl = verifiedConfig.Override.APIs(api.MakeAPIImplementation())
56-
r.RecipeImpl = verifiedConfig.Override.Functions(MakeRecipeImplementation(*querierInstance))
56+
var getEmailPasswordConfig = func() epmodels.TypeNormalisedInput {
57+
return verifiedConfig
58+
}
59+
r.RecipeImpl = verifiedConfig.Override.Functions(MakeRecipeImplementation(*querierInstance, getEmailPasswordConfig))
5760

5861
if emailDeliveryIngredient != nil {
5962
r.EmailDelivery = *emailDeliveryIngredient

recipe/emailpassword/recipeImplementation.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"github.com/supertokens/supertokens-golang/supertokens"
2121
)
2222

23-
func MakeRecipeImplementation(querier supertokens.Querier) epmodels.RecipeInterface {
23+
func MakeRecipeImplementation(querier supertokens.Querier, getEmailPasswordConfig func() epmodels.TypeNormalisedInput) epmodels.RecipeInterface {
2424
signUp := func(email, password string, userContext supertokens.UserContext) (epmodels.SignUpResponse, error) {
2525
response, err := querier.SendPostRequest("/recipe/signup", map[string]interface{}{
2626
"email": email,
@@ -158,14 +158,28 @@ func MakeRecipeImplementation(querier supertokens.Querier) epmodels.RecipeInterf
158158
}
159159
}
160160

161-
updateEmailOrPassword := func(userId string, email, password *string, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
161+
updateEmailOrPassword := func(userId string, email, password *string, applyPasswordPolicy *bool, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
162162
requestBody := map[string]interface{}{
163163
"userId": userId,
164164
}
165165
if email != nil {
166166
requestBody["email"] = email
167167
}
168168
if password != nil {
169+
if applyPasswordPolicy == nil || *applyPasswordPolicy {
170+
formFields := getEmailPasswordConfig().SignUpFeature.FormFields
171+
for i := range formFields {
172+
if formFields[i].ID == "password" {
173+
err := formFields[i].Validate(*password)
174+
if err != nil {
175+
errResponse := epmodels.PasswordPolicyViolatedError{
176+
FailureReason: *err,
177+
}
178+
return epmodels.UpdateEmailOrPasswordResponse{PasswordPolicyViolatedError: &errResponse}, nil
179+
}
180+
}
181+
}
182+
}
169183
requestBody["password"] = password
170184
}
171185
response, err := querier.SendPutRequest("/recipe/user", requestBody)

recipe/emailpassword/updateEmailPass_test.go

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,9 @@ func TestUpdateEmailPass(t *testing.T) {
9898
}
9999

100100
email := "test2@gmail.com"
101-
password := "testPass"
102-
103-
UpdateEmailOrPassword(data["user"].(map[string]interface{})["id"].(string), &email, &password)
101+
password := "testPass1"
104102

103+
UpdateEmailOrPassword(data["user"].(map[string]interface{})["id"].(string), &email, &password, nil)
105104
res1, err := unittesting.SignInRequest("testrandom@gmail.com", "validpass123", testServer.URL)
106105

107106
if err != nil {
@@ -140,6 +139,96 @@ func TestUpdateEmailPass(t *testing.T) {
140139

141140
assert.Equal(t, "OK", data2["status"])
142141
assert.Equal(t, email, data2["user"].(map[string]interface{})["email"])
142+
143+
password = "test"
144+
applyPasswordPolicy := true
145+
res3, err := UpdateEmailOrPassword(data["user"].(map[string]interface{})["id"].(string), &email, &password, &applyPasswordPolicy)
146+
assert.NotNil(t, res3.PasswordPolicyViolatedError)
147+
assert.Equal(t, "Password must contain at least 8 characters, including a number", res3.PasswordPolicyViolatedError.FailureReason)
148+
}
149+
150+
func TestUpdateEmailPassWithCustomValidator(t *testing.T) {
151+
configValue := supertokens.TypeInput{
152+
Supertokens: &supertokens.ConnectionInfo{
153+
ConnectionURI: "http://localhost:8080",
154+
},
155+
AppInfo: supertokens.AppInfo{
156+
APIDomain: "api.supertokens.io",
157+
AppName: "SuperTokens",
158+
WebsiteDomain: "supertokens.io",
159+
},
160+
RecipeList: []supertokens.Recipe{
161+
Init(&epmodels.TypeInput{SignUpFeature: &epmodels.TypeInputSignUp{FormFields: []epmodels.TypeInputFormField{
162+
{
163+
ID: "password",
164+
Validate: func(value interface{}) *string {
165+
if len(value.(string)) > 5 {
166+
return nil
167+
}
168+
err := "Password length must be more than 5 characters"
169+
return &err
170+
},
171+
},
172+
}}}),
173+
session.Init(&sessmodels.TypeInput{
174+
GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod {
175+
return sessmodels.CookieTransferMethod
176+
},
177+
}),
178+
},
179+
}
180+
181+
BeforeEach()
182+
unittesting.StartUpST("localhost", "8080")
183+
defer AfterEach()
184+
err := supertokens.Init(configValue)
185+
if err != nil {
186+
t.Error(err.Error())
187+
}
188+
querier, err := supertokens.GetNewQuerierInstanceOrThrowError("")
189+
if err != nil {
190+
t.Error(err.Error())
191+
}
192+
cdiVersion, err := querier.GetQuerierAPIVersion()
193+
if err != nil {
194+
t.Error(err.Error())
195+
}
196+
if unittesting.MaxVersion("2.7", cdiVersion) == "2.7" {
197+
return
198+
}
199+
mux := http.NewServeMux()
200+
testServer := httptest.NewServer(supertokens.Middleware(mux))
201+
defer testServer.Close()
202+
203+
_, err = unittesting.SignupRequest("testrandom@gmail.com", "validpass123", testServer.URL)
204+
if err != nil {
205+
t.Error(err.Error())
206+
}
207+
208+
res, err := unittesting.SignInRequest("testrandom@gmail.com", "validpass123", testServer.URL)
209+
210+
if err != nil {
211+
t.Error(err.Error())
212+
}
213+
dataInBytes, err := io.ReadAll(res.Body)
214+
if err != nil {
215+
t.Error(err.Error())
216+
}
217+
res.Body.Close()
218+
219+
var data map[string]interface{}
220+
err = json.Unmarshal(dataInBytes, &data)
221+
if err != nil {
222+
t.Error(err.Error())
223+
}
224+
225+
email := "testrandom@gmail.com"
226+
password := "test"
227+
228+
res1, err := UpdateEmailOrPassword(data["user"].(map[string]interface{})["id"].(string), &email, &password, nil)
229+
assert.NotNil(t, res1.PasswordPolicyViolatedError)
230+
assert.Equal(t, "Password length must be more than 5 characters", res1.PasswordPolicyViolatedError.FailureReason)
231+
143232
}
144233

145234
func TestAPICustomResponse(t *testing.T) {

recipe/thirdpartyemailpassword/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ func ResetPasswordUsingTokenWithContext(token, newPassword string, userContext s
9191
return (*instance.RecipeImpl.ResetPasswordUsingToken)(token, newPassword, userContext)
9292
}
9393

94-
func UpdateEmailOrPasswordWithContext(userId string, email *string, password *string, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
94+
func UpdateEmailOrPasswordWithContext(userId string, email *string, password *string, applyPasswordPolicy *bool, userContext supertokens.UserContext) (epmodels.UpdateEmailOrPasswordResponse, error) {
9595
instance, err := GetRecipeInstanceOrThrowError()
9696
if err != nil {
9797
return epmodels.UpdateEmailOrPasswordResponse{}, err
9898
}
99-
return (*instance.RecipeImpl.UpdateEmailOrPassword)(userId, email, password, userContext)
99+
return (*instance.RecipeImpl.UpdateEmailOrPassword)(userId, email, password, applyPasswordPolicy, userContext)
100100
}
101101

102102
func SendEmailWithContext(input emaildelivery.EmailType, userContext supertokens.UserContext) error {
@@ -139,8 +139,8 @@ func ResetPasswordUsingToken(token, newPassword string) (epmodels.ResetPasswordU
139139
return ResetPasswordUsingTokenWithContext(token, newPassword, &map[string]interface{}{})
140140
}
141141

142-
func UpdateEmailOrPassword(userId string, email *string, password *string) (epmodels.UpdateEmailOrPasswordResponse, error) {
143-
return UpdateEmailOrPasswordWithContext(userId, email, password, &map[string]interface{}{})
142+
func UpdateEmailOrPassword(userId string, email *string, password *string, applyPasswordPolicy *bool) (epmodels.UpdateEmailOrPasswordResponse, error) {
143+
return UpdateEmailOrPasswordWithContext(userId, email, password, applyPasswordPolicy, &map[string]interface{}{})
144144
}
145145

146146
func SendEmail(input emaildelivery.EmailType) error {

0 commit comments

Comments
 (0)