Skip to content

Commit

Permalink
Additional Regex for Username and Password
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnGrubba committed Nov 10, 2024
1 parent 987df4d commit c46b769
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 48 deletions.
4 changes: 3 additions & 1 deletion config/configtemplate.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"redirect_url": ""
},
"password_complexity": 4,
"username_complexity": 2
"password_regex": "\\s",
"username_complexity": 2,
"username_regex": "[^a-zA-Z0-9]"
},
"email": {
"login_usr": "",
Expand Down
6 changes: 4 additions & 2 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ Make sure that all parameters are set correctly before starting the service.
| `signup.oauth.providers_enabled` | **Datatype:** List <br> **Default:** `[]` <br> Enabled OAuth Providers. <br> **Possible Providers**<ul><li>[**Google**](../advanced/oauth.md#google-oauth)</li><li>[**GitHub**](../advanced/oauth.md#github-oauth)</li></ul> |
| `signup.oauth.base_url` | **Datatype:** String <br> **Default:** `"http://localhost:3250/"` <br> The Base URL for the callback URL from OAuth Providers. When you host the service somewhere, you may want to change this to the official Domain instead of an IP. This is also the value you set when setting up your OAuth Providers. Make sure those values match. (Mostly the URL of this instance unless you are overriding the callback function) |
| `signup.oauth.redirect_url` | **Datatype:** String <br> **Default:** `""` <br> Redirect URL after the callback has been called and account was created or logged in. Only makes sense when using EZAuth Client Side or on the same domain. (Cookie may be set only for the EZAuth Domain if different than your frontend) |
| `signup.password_complexity` | **Datatype:** Integer <br> **Default:** `4` <br> Password Complexity Requirement. Every higher value, includes all the previous ones too.<br> <ul><li>**1**: Minimum 8 Characters</li><li>**2**: Min. One Digit</li><li>**3**: Min. One Capital Letter</li><li>**4**: Min. One Special Character</li></ul> |
| `signup.username_complexity` | **Datatype:** Integer <br> **Default:** `2` <br> Username Complexity Requirement. Every higher value, includes all the previous ones too.<br> <ul><li>**1**: Minimum 4 Characters</li><li>**2**: Max. 20 Characters</li></ul> |
| `signup.password_complexity` | **Datatype:** Integer <br> **Default:** `4` <br> Password Complexity Requirement. Every higher value, includes all the previous ones too.<br> <ul><li>**0**: No Restrictions</li><li>**1**: Minimum 8 Characters</li><li>**2**: Min. One Digit</li><li>**3**: Min. One Capital Letter</li><li>**4**: Min. One Special Character</li></ul> |
| `signup.password_regex` | **Datatype:** String <br> **Default:** `\\s` <br> Additional Regex for Password validation. Default doesn't allow whitespaces in password. Should be used when `signup.password_complexity` is set to 0. |
| `signup.username_complexity` | **Datatype:** Integer <br> **Default:** `2` <br> Username Complexity Requirement. Every higher value, includes all the previous ones too.<br> <ul><li>**0**: No Restrictions</li><li>**1**: Minimum 4 Characters</li><li>**2**: Max. 20 Characters</li></ul> |
| `signup.username_regex` | **Datatype:** String <br> **Default:** `[^a-zA-Z0-9]` <br> Additional Regex for Username validation. Default value restricts special characters. Should be used when `signup.username_complexity` is set to 0. |

### E-Mail Configuration

Expand Down
52 changes: 52 additions & 0 deletions src/api/helpers/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from tools import SignupConfig
import re
import re
import bcrypt
from pydantic import SecretStr


def username_check(cls, username: str) -> str:
if len(username) == 0:
raise ValueError("Username cannot be empty")
if len(username) < 4:
if SignupConfig.username_complexity >= 1:
raise ValueError("Username must be at least 4 characters long")
if len(username) > 20:
if SignupConfig.username_complexity >= 2:
raise ValueError("Username must be at most 20 characters long")

# Additional Regex Checks
if re.search(SignupConfig.username_regex, username) != None:
raise ValueError("Username must only contain letters and numbers")

return username


def password_check_hash(cls, password: SecretStr) -> str:
# Validate Password
pswd = password.get_secret_value()
if len(pswd) == 0:
raise ValueError("Password cannot be empty")
if len(pswd) < 8 and SignupConfig.password_complexity >= 1:
raise ValueError("Make sure your password has at least 8 letters")
if re.search("[0-9]", pswd) is None and SignupConfig.password_complexity >= 2:
raise ValueError("Make sure your password has a number in it")
if re.search("[A-Z]", pswd) is None and SignupConfig.password_complexity >= 3:
raise ValueError("Make sure your password has a capital letter in it")
if (
re.search("[^a-zA-Z0-9]", pswd) is None
and SignupConfig.password_complexity >= 4
):
raise ValueError("Make sure your password has a special character in it")
if len(pswd) > 50:
raise ValueError("Make sure your password is at most 50 characters")

# Additional Regex Checks
if re.search(SignupConfig.password_regex, pswd) != None:
raise ValueError("Password does not meet complexity requirements")

# Hash Password
hashed_pswd = bcrypt.hashpw(pswd.encode("utf-8"), bcrypt.gensalt(10)).decode(
"utf-8"
)
return hashed_pswd
43 changes: 5 additions & 38 deletions src/api/model.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from pydantic import BaseModel, field_validator, EmailStr, SecretStr, ConfigDict, Field
from typing import Optional, List
from tools import SignupConfig
import re
import bcrypt
from api.helpers.validators import username_check, password_check_hash


class InternalUserQuery(BaseModel):
Expand Down Expand Up @@ -59,48 +57,17 @@ class PasswordHashed(BaseModel):

@field_validator("password")
@classmethod
def password_check_hash(cls, password: SecretStr) -> str:
# Validate Password
pswd = password.get_secret_value()
if len(pswd) == 0:
raise ValueError("Password cannot be empty")
if len(pswd) < 8 and SignupConfig.password_complexity >= 1:
raise ValueError("Make sure your password has at least 8 letters")
elif re.search("[0-9]", pswd) is None and SignupConfig.password_complexity >= 2:
raise ValueError("Make sure your password has a number in it")
elif re.search("[A-Z]", pswd) is None and SignupConfig.password_complexity >= 3:
raise ValueError("Make sure your password has a capital letter in it")
elif (
re.search("[^a-zA-Z0-9]", pswd) is None
and SignupConfig.password_complexity >= 4
):
raise ValueError("Make sure your password has a special character in it")
elif len(pswd) > 50:
raise ValueError("Make sure your password is at most 50 characters")
# Hash Password
hashed_pswd = bcrypt.hashpw(pswd.encode("utf-8"), bcrypt.gensalt(10)).decode(
"utf-8"
)
return hashed_pswd
def pswdcheck(cls, password: SecretStr) -> str:
return password_check_hash(cls, password)


class Username(BaseModel):
username: str

@field_validator("username")
@classmethod
def username_check(cls, username: str) -> str:
if len(username) == 0:
raise ValueError("Username cannot be empty")
if len(username) < 4:
if SignupConfig.username_complexity >= 1:
raise ValueError("Username must be at least 4 characters long")
if len(username) > 20:
if SignupConfig.username_complexity >= 2:
raise ValueError("Username must be at most 20 characters long")
elif re.search("[^a-zA-Z0-9]", username) is not None:
raise ValueError("Username must only contain letters and numbers")
return username
def usrchck(cls, username: str) -> str:
return username_check(cls, username)


class DeleteAccountRequest(BaseModel):
Expand Down
13 changes: 7 additions & 6 deletions src/api/signup_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,42 +138,43 @@ def test_create_account_invalid_username_too_short():
"/signup",
json={
"password": "Kennwort1!",
"email": "user1@example.com",
"email": "user13@example.com",
"username": "SAS",
},
)
assert response.status_code == 422
assert (
response.json()["detail"][0]["msg"]
== "Value error, Username must be at least 4 characters long"
)
assert response.status_code == 422


def test_create_account_invalid_username_special_cases():
response = client.post(
"/signup",
json={
"password": "Kennwort1!",
"email": "user1@example.com",
"email": "user14@example.com",
"username": "<div></div>",
},
)
assert response.status_code == 422
assert (
response.json()["detail"][0]["msg"]
== "Value error, Username must only contain letters and numbers"
)
assert response.status_code == 422


def test_create_account_invalid_username_too_long():
response = client.post(
"/signup",
json={
"password": "Kennwort1!",
"email": "user1@example.com",
"username": "adsjfölksajdflkasjdlkfjasdlkfjlksajdflöksajdfölkjsadfkjasölkfj",
"email": "user12@example.com",
"username": "adsjfölksajdflkasjdlkfjasdlaakfjlksajdflöksajdfölkjsadfkjasölkfj",
},
)
assert response.status_code == 422
assert (
response.json()["detail"][0]["msg"]
== "Value error, Username must be at most 20 characters long"
Expand Down
31 changes: 31 additions & 0 deletions src/tools/conf/SignupConfig.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .conf import config
import re


class SignupConfig:
Expand All @@ -13,7 +14,9 @@ class SignupConfig:
config["signup"]["oauth"]["redirect_url"]
).removesuffix("/")
password_complexity: int = config["signup"]["password_complexity"]
password_regex: str = config["signup"]["password_regex"]
username_complexity: int = config["signup"]["username_complexity"]
username_regex: str = config["signup"]["username_regex"]

def validate_types(self) -> bool:
"""This is to Type Check the Configuration"""
Expand Down Expand Up @@ -56,19 +59,35 @@ def validate_types(self) -> bool:
)
if not all(isinstance(i, str) for i in self.oauth_providers):
raise ValueError("signup.oauth.providers_enabled must be a list of strings")

if not isinstance(self.password_complexity, int):
raise ValueError(
"signup.password_complexity must be an integer (got type {})".format(
type(self.password_complexity)
)
)

if not isinstance(self.password_regex, str):
raise ValueError(
"signup.password_regex must be a string (got type {})".format(
type(self.password_regex)
)
)

if not isinstance(self.username_complexity, int):
raise ValueError(
"signup.username_complexity must be an integer (got type {})".format(
type(self.username_complexity)
)
)

if not isinstance(self.username_regex, str):
raise ValueError(
"signup.username_regex must be a string (got type {})".format(
type(self.username_regex)
)
)

def validate_values(self) -> bool:
"""This is to Value Check the Configuration"""
if not self.conf_code_expiry > 0:
Expand All @@ -95,10 +114,22 @@ def validate_values(self) -> bool:
raise ValueError(
f"signup.password_complexity must be between 1 and 4 (Check Docs), got {self.password_complexity}"
)
try:
re.compile(self.password_regex)
except re.error:
raise ValueError(
f"signup.password_regex is invalid, got {self.password_regex}"
)
if self.username_complexity not in range(1, 3):
raise ValueError(
f"signup.username_complexity must be 1 or 2 (Check Docs), got {self.username_complexity}"
)
try:
re.compile(self.username_regex)
except re.error:
raise ValueError(
f"signup.username_regex is invalid, got {self.username_regex}"
)


SignupConfig().validate_types()
Expand Down
4 changes: 3 additions & 1 deletion src/tools/conf/testing_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"redirect_url": ""
},
"password_complexity": 4,
"username_complexity": 2
"password_regex": "\\s",
"username_complexity": 2,
"username_regex": "[^a-zA-Z0-9]"
},
"email": {
"login_usr": "",
Expand Down

0 comments on commit c46b769

Please sign in to comment.