Skip to content

Commit d2b8860

Browse files
Merge pull request #319 from supertokens/passwordPolicyOnUpgrade
feat: optional password validation in update_email_or_password
2 parents 9e60f3d + afdc158 commit d2b8860

File tree

15 files changed

+224
-15
lines changed

15 files changed

+224
-15
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.13.0] - 2023-05-03
11+
12+
- added optional password policy check in `update_email_or_password`
1013

1114
## [0.12.9] - 2023-04-28
1215

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070

7171
setup(
7272
name="supertokens_python",
73-
version="0.12.9",
73+
version="0.13.0",
7474
author="SuperTokens",
7575
license="Apache 2.0",
7676
author_email="[email protected]",

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"2.19",
2626
"2.20",
2727
]
28-
VERSION = "0.12.9"
28+
VERSION = "0.13.0"
2929
TELEMETRY = "/telemetry"
3030
USER_COUNT = "/users/count"
3131
USER_DELETE = "/user/remove"

supertokens_python/recipe/emailpassword/asyncio/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ async def update_email_or_password(
2222
user_id: str,
2323
email: Union[str, None] = None,
2424
password: Union[str, None] = None,
25+
apply_password_policy: Union[bool, None] = None,
2526
user_context: Union[None, Dict[str, Any]] = None,
2627
):
2728
if user_context is None:
2829
user_context = {}
2930
return await EmailPasswordRecipe.get_instance().recipe_implementation.update_email_or_password(
30-
user_id, email, password, user_context
31+
user_id, email, password, apply_password_policy, user_context
3132
)
3233

3334

supertokens_python/recipe/emailpassword/interfaces.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ class UpdateEmailOrPasswordUnknownUserIdError:
7878
pass
7979

8080

81+
class UpdateEmailOrPasswordPasswordPolicyViolationError:
82+
failure_reason: str
83+
84+
def __init__(self, failure_reason: str):
85+
self.failure_reason = failure_reason
86+
87+
8188
class RecipeInterface(ABC):
8289
def __init__(self):
8390
pass
@@ -126,11 +133,13 @@ async def update_email_or_password(
126133
user_id: str,
127134
email: Union[str, None],
128135
password: Union[str, None],
136+
apply_password_policy: Union[bool, None],
129137
user_context: Dict[str, Any],
130138
) -> Union[
131139
UpdateEmailOrPasswordOkResult,
132140
UpdateEmailOrPasswordEmailAlreadyExistsError,
133141
UpdateEmailOrPasswordUnknownUserIdError,
142+
UpdateEmailOrPasswordPasswordPolicyViolationError,
134143
]:
135144
pass
136145

supertokens_python/recipe/emailpassword/recipe.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
InputResetPasswordUsingTokenFeature,
6565
InputSignUpFeature,
6666
validate_and_normalise_user_input,
67+
EmailPasswordConfig,
6768
)
6869

6970

@@ -92,7 +93,13 @@ def __init__(
9293
override,
9394
email_delivery,
9495
)
95-
recipe_implementation = RecipeImplementation(Querier.get_instance(recipe_id))
96+
97+
def get_emailpassword_config() -> EmailPasswordConfig:
98+
return self.config
99+
100+
recipe_implementation = RecipeImplementation(
101+
Querier.get_instance(recipe_id), get_emailpassword_config
102+
)
96103
self.recipe_implementation = (
97104
recipe_implementation
98105
if self.config.override.functions is None

supertokens_python/recipe/emailpassword/recipe_implementation.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# under the License.
1414
from __future__ import annotations
1515

16-
from typing import TYPE_CHECKING, Any, Dict, Union
16+
from typing import TYPE_CHECKING, Any, Dict, Union, Callable
1717

1818
from supertokens_python.normalised_url_path import NormalisedURLPath
1919

@@ -30,17 +30,25 @@
3030
UpdateEmailOrPasswordEmailAlreadyExistsError,
3131
UpdateEmailOrPasswordOkResult,
3232
UpdateEmailOrPasswordUnknownUserIdError,
33+
UpdateEmailOrPasswordPasswordPolicyViolationError,
3334
)
3435
from .types import User
36+
from .utils import EmailPasswordConfig
37+
from .constants import FORM_FIELD_PASSWORD_ID
3538

3639
if TYPE_CHECKING:
3740
from supertokens_python.querier import Querier
3841

3942

4043
class RecipeImplementation(RecipeInterface):
41-
def __init__(self, querier: Querier):
44+
def __init__(
45+
self,
46+
querier: Querier,
47+
get_emailpassword_config: Callable[[], EmailPasswordConfig],
48+
):
4249
super().__init__()
4350
self.querier = querier
51+
self.get_emailpassword_config = get_emailpassword_config
4452

4553
async def get_user_by_id(
4654
self, user_id: str, user_context: Dict[str, Any]
@@ -138,16 +146,28 @@ async def update_email_or_password(
138146
user_id: str,
139147
email: Union[str, None],
140148
password: Union[str, None],
149+
apply_password_policy: Union[bool, None],
141150
user_context: Dict[str, Any],
142151
) -> Union[
143152
UpdateEmailOrPasswordOkResult,
144153
UpdateEmailOrPasswordEmailAlreadyExistsError,
145154
UpdateEmailOrPasswordUnknownUserIdError,
155+
UpdateEmailOrPasswordPasswordPolicyViolationError,
146156
]:
147157
data = {"userId": user_id}
148158
if email is not None:
149159
data = {"email": email, **data}
150160
if password is not None:
161+
if apply_password_policy is None or apply_password_policy:
162+
form_fields = (
163+
self.get_emailpassword_config().sign_up_feature.form_fields
164+
)
165+
password_field = list(
166+
filter(lambda x: x.id == FORM_FIELD_PASSWORD_ID, form_fields)
167+
)[0]
168+
error = await password_field.validate(password)
169+
if error is not None:
170+
return UpdateEmailOrPasswordPasswordPolicyViolationError(error)
151171
data = {"password": password, **data}
152172
response = await self.querier.send_put_request(
153173
NormalisedURLPath("/recipe/user"), data

supertokens_python/recipe/emailpassword/syncio/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ def update_email_or_password(
2323
user_id: str,
2424
email: Union[str, None] = None,
2525
password: Union[str, None] = None,
26+
apply_password_policy: Union[bool, None] = None,
2627
user_context: Union[None, Dict[str, Any]] = None,
2728
):
2829
from supertokens_python.recipe.emailpassword.asyncio import update_email_or_password
2930

30-
return sync(update_email_or_password(user_id, email, password, user_context))
31+
return sync(
32+
update_email_or_password(
33+
user_id, email, password, apply_password_policy, user_context
34+
)
35+
)
3136

3237

3338
def get_user_by_id(

supertokens_python/recipe/thirdpartyemailpassword/asyncio/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,13 @@ async def update_email_or_password(
100100
user_id: str,
101101
email: Union[None, str] = None,
102102
password: Union[None, str] = None,
103+
apply_password_policy: Union[bool, None] = None,
103104
user_context: Union[None, Dict[str, Any]] = None,
104105
):
105106
if user_context is None:
106107
user_context = {}
107108
return await ThirdPartyEmailPasswordRecipe.get_instance().recipe_implementation.update_email_or_password(
108-
user_id, email, password, user_context
109+
user_id, email, password, apply_password_policy, user_context
109110
)
110111

111112

supertokens_python/recipe/thirdpartyemailpassword/interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
UpdateEmailOrPasswordUnknownUserIdError = (
4444
EPInterfaces.UpdateEmailOrPasswordUnknownUserIdError
4545
)
46+
UpdateEmailOrPasswordPasswordPolicyViolationError = (
47+
EPInterfaces.UpdateEmailOrPasswordPasswordPolicyViolationError
48+
)
4649

4750
AuthorisationUrlGetOkResult = ThirdPartyInterfaces.AuthorisationUrlGetOkResult
4851
ThirdPartySignInUpPostNoEmailGivenByProviderResponse = (
@@ -133,11 +136,13 @@ async def update_email_or_password(
133136
user_id: str,
134137
email: Union[str, None],
135138
password: Union[str, None],
139+
apply_password_policy: Union[bool, None],
136140
user_context: Dict[str, Any],
137141
) -> Union[
138142
UpdateEmailOrPasswordOkResult,
139143
UpdateEmailOrPasswordEmailAlreadyExistsError,
140144
UpdateEmailOrPasswordUnknownUserIdError,
145+
UpdateEmailOrPasswordPasswordPolicyViolationError,
141146
]:
142147
pass
143148

supertokens_python/recipe/thirdpartyemailpassword/recipe.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
from ..emailpassword.interfaces import APIInterface as EmailPasswordAPIInterface
6868
from ..emailpassword.interfaces import RecipeInterface as EmailPasswordRecipeInterface
69+
from ..emailpassword.utils import EmailPasswordConfig
6970
from ..thirdparty.interfaces import APIInterface as ThirdPartyAPIInterface
7071
from ..thirdparty.interfaces import RecipeInterface as ThirdPartyRecipeInterface
7172
from .exceptions import SupertokensThirdPartyEmailPasswordError
@@ -103,9 +104,13 @@ def __init__(
103104
email_delivery,
104105
)
105106

107+
def get_emailpassword_config() -> EmailPasswordConfig:
108+
return self.email_password_recipe.config
109+
106110
recipe_implementation = RecipeImplementation(
107111
Querier.get_instance(EmailPasswordRecipe.recipe_id),
108112
Querier.get_instance(ThirdPartyRecipe.recipe_id),
113+
get_emailpassword_config,
109114
)
110115
self.recipe_implementation: RecipeInterface = (
111116
recipe_implementation

supertokens_python/recipe/thirdpartyemailpassword/recipeimplementation/email_password_recipe_implementation.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
UpdateEmailOrPasswordEmailAlreadyExistsError,
2929
UpdateEmailOrPasswordOkResult,
3030
UpdateEmailOrPasswordUnknownUserIdError,
31+
UpdateEmailOrPasswordPasswordPolicyViolationError,
3132
)
3233
from supertokens_python.recipe.emailpassword.types import User
3334

@@ -39,7 +40,10 @@
3940

4041

4142
class RecipeImplementation(RecipeInterface):
42-
def __init__(self, recipe_implementation: ThirdPartyEmailPasswordRecipeInterface):
43+
def __init__(
44+
self,
45+
recipe_implementation: ThirdPartyEmailPasswordRecipeInterface,
46+
):
4347
super().__init__()
4448
self.recipe_implementation = recipe_implementation
4549

@@ -113,12 +117,14 @@ async def update_email_or_password(
113117
user_id: str,
114118
email: Union[str, None],
115119
password: Union[str, None],
120+
apply_password_policy: Union[bool, None],
116121
user_context: Dict[str, Any],
117122
) -> Union[
118123
UpdateEmailOrPasswordOkResult,
119124
UpdateEmailOrPasswordEmailAlreadyExistsError,
120125
UpdateEmailOrPasswordUnknownUserIdError,
126+
UpdateEmailOrPasswordPasswordPolicyViolationError,
121127
]:
122128
return await self.recipe_implementation.update_email_or_password(
123-
user_id, email, password, user_context
129+
user_id, email, password, apply_password_policy, user_context
124130
)

supertokens_python/recipe/thirdpartyemailpassword/recipeimplementation/implementation.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# under the License.
1414
from __future__ import annotations
1515

16-
from typing import TYPE_CHECKING, Any, Dict, List, Union
16+
from typing import TYPE_CHECKING, Any, Dict, List, Union, Callable
1717

1818
import supertokens_python.recipe.emailpassword.interfaces as EPInterfaces
1919

@@ -41,6 +41,7 @@
4141
UpdateEmailOrPasswordEmailAlreadyExistsError,
4242
UpdateEmailOrPasswordOkResult,
4343
UpdateEmailOrPasswordUnknownUserIdError,
44+
UpdateEmailOrPasswordPasswordPolicyViolationError,
4445
)
4546
from ..types import User
4647
from .email_password_recipe_implementation import (
@@ -49,15 +50,19 @@
4950
from .third_party_recipe_implementation import (
5051
RecipeImplementation as DerivedThirdPartyImplementation,
5152
)
53+
from supertokens_python.recipe.emailpassword.utils import EmailPasswordConfig
5254

5355

5456
class RecipeImplementation(RecipeInterface):
5557
def __init__(
56-
self, emailpassword_querier: Querier, thirdparty_querier: Union[Querier, None]
58+
self,
59+
emailpassword_querier: Querier,
60+
thirdparty_querier: Union[Querier, None],
61+
get_emailpassword_config: Callable[[], EmailPasswordConfig],
5762
):
5863
super().__init__()
5964
emailpassword_implementation = EmailPasswordImplementation(
60-
emailpassword_querier
65+
emailpassword_querier, get_emailpassword_config
6166
)
6267

6368
self.ep_get_user_by_id = emailpassword_implementation.get_user_by_id
@@ -262,11 +267,13 @@ async def update_email_or_password(
262267
user_id: str,
263268
email: Union[None, str],
264269
password: Union[None, str],
270+
apply_password_policy: Union[bool, None],
265271
user_context: Dict[str, Any],
266272
) -> Union[
267273
UpdateEmailOrPasswordOkResult,
268274
UpdateEmailOrPasswordEmailAlreadyExistsError,
269275
UpdateEmailOrPasswordUnknownUserIdError,
276+
UpdateEmailOrPasswordPasswordPolicyViolationError,
270277
]:
271278
user = await self.get_user_by_id(user_id, user_context)
272279
if user is None:
@@ -276,5 +283,5 @@ async def update_email_or_password(
276283
"Cannot update email or password of a user who signed up using third party login."
277284
)
278285
return await self.ep_update_email_or_password(
279-
user_id, email, password, user_context
286+
user_id, email, password, apply_password_policy, user_context
280287
)

supertokens_python/recipe/thirdpartyemailpassword/syncio/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,18 @@ def update_email_or_password(
104104
user_id: str,
105105
email: Union[None, str] = None,
106106
password: Union[None, str] = None,
107+
apply_password_policy: Union[bool, None] = None,
107108
user_context: Union[None, Dict[str, Any]] = None,
108109
):
109110
from supertokens_python.recipe.thirdpartyemailpassword.asyncio import (
110111
update_email_or_password,
111112
)
112113

113-
return sync(update_email_or_password(user_id, email, password, user_context))
114+
return sync(
115+
update_email_or_password(
116+
user_id, email, password, apply_password_policy, user_context
117+
)
118+
)
114119

115120

116121
def get_users_by_email(

0 commit comments

Comments
 (0)