Skip to content

Commit

Permalink
Merge pull request #2 from JohnGrubba/dev
Browse files Browse the repository at this point in the history
Session Management + Welcome E-Mail + SignUp E-Mail
  • Loading branch information
JohnGrubba authored Jul 1, 2024
2 parents 471a024 + 01cc4c6 commit 2a8b054
Show file tree
Hide file tree
Showing 19 changed files with 320 additions and 41 deletions.
4 changes: 4 additions & 0 deletions config/configtemplate.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"signup": {
"enable_conf_email": false,
"conf_code_expiry": 5,
"conf_code_complexity": 1,
"enable_welcome_email": true
},
"email": {
Expand All @@ -10,5 +11,8 @@
"sender_email": "",
"smtp_host": "",
"smtp_port": 465
},
"session": {
"session_expiry_seconds": 86400
}
}
2 changes: 1 addition & 1 deletion config/email/ConfirmEmail.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<body class="clean-body u_body"
style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #ecf0f1;color: #000000">
<h1>Did you just sign up as {username}</h1>
<h1>Did you just sign up as {username}?</h1>
<p>If it was you, then enter this code to confirm your email address:</p>
<h2>{code}</h2>
<p>If it wasn't you, then ignore this email.</p>
Expand Down
1 change: 1 addition & 0 deletions config/email/WelcomeEmail.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<body class="clean-body u_body"
style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #ecf0f1;color: #000000">
<h1>This is a Template Welcome Message. Welcome to EZAuth {username}</h1>
<p>Thank you for signing up with us. We are excited to have you on board.</p>
</body>

</html>
28 changes: 28 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
api:
container_name: ezauth-api
build: "./src"
ports:
- "3250:80"
environment:
- DATABASE_URL=mongodb://admin:admin@db/ezauth?authSource=admin&retryWrites=true&w=majority
restart: always
volumes:
- ./config:/src/app/config
- ./src:/src/app
command: |
bash -c 'uvicorn api.main:app --reload --host 0.0.0.0 --port 80 --log-level info'
db:
image: mongo
ports:
- '27017:27017'
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=admin
- MONGO_INITDB_DATABASE=ezauth
volumes:
- ezauth-data:/data/db

volumes:
ezauth-data:
15 changes: 11 additions & 4 deletions docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@ Make sure that all parameters are set correctly before starting the service.
|------------|-------------|
| `signup.enable_conf_email` | **Datatype:** Boolean <br> **Default:** `false` <br> Enable or disable the confirmation E-Mail for new users. |
| `signup.conf_code_expiry` | **Datatype:** Integer <br> **Default:** `5` <br> The time in minutes until the confirmation code expires. |
| `signup.conf_code_complexity` | **Datatype:** Integer <br> **Default:** `1` <br> The complexity of the confirmation code. <br> **Possible Values** <br> <ul><li>**1**: `4 Digit Numeric`</li><li>**2**: `6 Digit Numeric`</li><li>**3**: `4 Characters`</li><li>**4**: `6 Characters`</li></ul> |
| `signup.enable_welcome_email` | **Datatype:** Boolean <br> **Default:** `false` <br> Enable or disable the welcome E-Mail for new users. |

### Email Configuration

### E-Mail Configuration

??? Warning "SMTP SSL required"
EZAuth uses SMTP_SSL to send E-Mails. Make sure that your SMTP server supports SSL.
Currently EZAuth does not support STARTTLS.

| Parameter | Description |
|------------|-------------|
| `email.login_usr` | **Datatype:** String <br> **Default:** `""` <br> E-Mail Login Identifier (mostly the email itself). <br> **Example:** [email protected] |
| `email.login_usr` | **Datatype:** String <br> **Default:** `""` <br> E-Mail Login Identifier (mostly the E-Mail itself). <br> **Example:** [email protected] |
| `email.login_pwd` | **Datatype:** String <br> **Default:** `""` <br> E-Mail Login Password. |
| `email.sender_email` | **Datatype:** String <br> **Default:** `""` <br> E-Mail address from which the emails are sent (mostly the same as `email.login_usr`) |
| `email.sender_email` | **Datatype:** String <br> **Default:** `""` <br> E-Mail address from which the E-Mails are sent (mostly the same as `email.login_usr`) |
| `email.smtp_host` | **Datatype:** String <br> **Default:** `""` <br> SMTP Host for the E-Mail server. <br> **Example:** `smtp.gmail.com` |
| `email.smtp_port` | **Datatype:** Integer <br> **Default:** `465` <br> SMTP Port for the E-Mail server. |
| `email.smtp_port` | **Datatype:** Integer <br> **Default:** `465` <br> SMTP Port for the E-Mail server. |

### Session Configuration
| Parameter | Description |
|------------|-------------|
| `session.session_expiry_seconds` | **Datatype:** Integer <br> **Default:** `86400` <br> The time in seconds until a login session expires. |
7 changes: 4 additions & 3 deletions docs/getting-started/email_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ EZAuth uses a set of default E-mail templates to send out e-mails to users. Thes
!!! Info "E-Mail Subject"
EZAuth will automatically use the HTML `<title></title>` tag to specify the subject of the e-mail.

!!! Info "Default Placeholders"
Every E-Mail template which is directed at a registered user will be able to use any of the properties of the user in the database. This includes the `username`, `email`, `id`, and any other property you might have added to the user object.

## Required E-Mail Templates

??? Warning "Required Templates"
Expand All @@ -17,9 +20,7 @@ EZAuth uses a set of default E-mail templates to send out e-mails to users. Thes
#### Additional Placeholders
- `{{code}}`: The confirmation code to confirm the email address.
- `{{time}}`: Time remaining before the confirmation code expires in minutes. (e.g. 5)

!!! Info "Default Placeholders"
Every E-Mail template which is directed at a registered user will be able to use any of the properties of the user in the database. This includes the `username`, `email`, `id`, and any other property you might have added to the user object.
- `{{username}}`: The username of the user wanting to verify their email address.

### 2. **Welcome Email**
- Can be enabled in the `config.json` file.
Expand Down
65 changes: 65 additions & 0 deletions docs/information.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Information about EZAuth

EZAuth is a simple and easy-to-use authentication service for your applications. It is built on top of [FastAPI](https://fastapi.tiangolo.com/)
and [MongoDB](https://www.mongodb.com/).

## Security
To guarantee the security of your user data, EZAuth uses the following security measures:

- **Password Hashing**: All passwords are hashed using the `bcrypt` algorithm.
- **Session Management**: Sessions can be configured to expire after a certain amount of time.
- **E-Mail Verification**: Users can verify their email address before using the service.
- **2FA**: Two-factor authentication can be enabled for users.
- **OAuth2**: OAuth2 can be enabled for users.

## Usage

!!! Info "Official Libraries"
We are working on official Libraries for different languages. Until then, you can use the REST API to interact with the service.
Every help is appreciated.


You can use EZAuth in any Application.
Here are some examples in different languages:

### Python
```py linenums="1"
import requests

url = "http://localhost:3250/signup/"

payload = {
"email": "[email protected]",
"username": "Hans",
"password": "Kennwort1!",
}

requests.post(url, json=payload)
```

### JavaScript
```js linenums="1"
fetch("http://localhost:3250/signup/", {
method: 'POST',
headers: {
"Content-Type", "application/json"
},
body: JSON.stringify({
"email": "[email protected]",
"username": "Hans",
"password": "Kennwort1!"
})
})
```

### cURL
```bash
curl -X 'POST'
'http://localhost:3250/signup/'
-H 'Content-Type: application/json'
-d '{
"email": "[email protected]",
"username": "Hans",
"password": "Kennwort1!"
}'
```
20 changes: 6 additions & 14 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ nav:
- Installation / Setup: getting-started/setup.md
- Basic Configuration: getting-started/configuration.md
- E-Mail Templates: getting-started/email_config.md
- Information: information.md
theme:
name: material
logo: "ezauth_logo.png"
Expand Down Expand Up @@ -40,19 +41,10 @@ markdown_extensions:
guess_lang: false
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.pathconverter
- pymdownx.smartsymbols
- pymdownx.snippets:
base_path: docs
check_paths: true
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
Empty file added src/api/login.py
Empty file.
5 changes: 4 additions & 1 deletion src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

app = FastAPI(
title="EZAuth API",
description="EZAuth is a high performance self-hosted and fully customizable authentication service",
description="""
<img src="https://johngrubba.github.io/ezauth/ezauth_banner.png" />
<h2> EZAuth is a high performance self-hosted and fully customizable authentication service </h2>
""",
)

origins = ["*"]
Expand Down
23 changes: 22 additions & 1 deletion src/api/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
from pydantic import BaseModel, field_validator, EmailStr, SecretStr, ConfigDict
import re
import bcrypt


class LoginRequest(BaseModel):
identifier: str
password: SecretStr


class LoginResponse(BaseModel):
session_token: str


class ConfirmEmailCodeRequest(BaseModel):
code: int | str
email: str


class UserSignupRequest(BaseModel):
Expand All @@ -13,7 +28,8 @@ class UserSignupRequest(BaseModel):

@field_validator("password")
@classmethod
def password_validator(cls, password: SecretStr) -> str:
def password_check_hash(cls, password: SecretStr) -> str:
# Validate Password
pswd = password.get_secret_value()
if len(pswd) < 8:
raise ValueError("Make sure your password is at least 8 letters")
Expand All @@ -23,3 +39,8 @@ def password_validator(cls, password: SecretStr) -> str:
raise ValueError("Make sure your password has a capital letter in it")
elif re.search("[^a-zA-Z0-9]", pswd) is None:
raise ValueError("Make sure your password has a special character in it")
# Hash Password
hashed_pswd = bcrypt.hashpw(pswd.encode("utf-8"), bcrypt.gensalt(5)).decode(
"utf-8"
)
return hashed_pswd
Empty file added src/api/profile.py
Empty file.
97 changes: 83 additions & 14 deletions src/api/signup.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,74 @@
from fastapi import APIRouter
from api.model import UserSignupRequest
from tools import users_collection
from tools.mail import send_email
from fastapi import APIRouter, Response, BackgroundTasks
from api.model import UserSignupRequest, LoginResponse, ConfirmEmailCodeRequest
from tools import send_email
from tools import SignupConfig
from expiring_dict import ExpiringDict
import random
from crud.user import create_user, check_unique_usr

# Create an ExpiringDict object to store temporary accounts (not email verified yet)
"""
temp_accounts["form"]: UserSignupRequest
temp_accounts["code"]: str | int
"""
temp_accounts = ExpiringDict(ttl=SignupConfig.conf_code_expiry * 60, interval=10)

# Generate and shuffle 10000 unique IDs for confirmation email
all_ids = [f"{i:04d}" for i in range(10000)]
random.shuffle(all_ids)

# Generate and shuffle 10000 unique IDs for confirmation email (Depending on complexity)
match (SignupConfig.conf_code_complexity):
case 2:
# Random 6 Digit Numbers
all_ids = [str(random.randint(100000, 999999)) for _ in range(10000)]
random.shuffle(all_ids)
case 3:
# Random 4 Character Strings
all_ids = [
"".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=4))
for _ in range(10000)
]
random.shuffle(all_ids)
case 4:
# Random 6 Character Strings
all_ids = [
"".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=6))
for _ in range(10000)
]
random.shuffle(all_ids)
case _:
# Default Case (1)
all_ids = [str(i) for i in range(10000)]
random.shuffle(all_ids)

router = APIRouter(
prefix="/signup",
tags=["Sign Up"],
responses={404: {"description": "Not found"}},
dependencies=[],
)


@router.post("/", status_code=204)
async def signup(signup_form: UserSignupRequest):
@router.post(
"/",
status_code=200,
responses={
409: {"description": "Duplicate Entry"},
204: {"description": "Confirmation Email Sent"},
200: {"description": "Account was created successfully."},
},
)
async def signup(signup_form: UserSignupRequest, background_tasks: BackgroundTasks):
# Handle signup
if SignupConfig.enable_conf_email:
# Those checks are only needed when confirmation emails are enabled (otherwise, create the user directly and raise duplicate from mongodb)
# Check if email in confirmation email dict
try:
temp_accounts[signup_form.email]
except KeyError:
pass
else:
return Response("E-Mail already sent.", status_code=409)
# Check if user already exists in database
if check_unique_usr(signup_form.email, signup_form.username):
return Response("Email or Username already exists.", status_code=409)
# If all numbers have been used, raise an exception
if not all_ids:
raise Exception(
Expand All @@ -32,16 +77,40 @@ async def signup(signup_form: UserSignupRequest):
# Get a unique ID for confirmation email
unique_id = all_ids.pop()
# Save the Account into the expiring dict (delete if user refuses to confirm email in time)
temp_accounts[unique_id] = signup_form
# Indexed by E-Mail to quickly check if the user has already signed up (O(1))
temp_accounts[signup_form.email] = {"form": signup_form, "code": unique_id}

# Generate and send confirmation email
send_email(
background_tasks.add_task(
send_email,
"ConfirmEmail",
signup_form.email,
code=unique_id,
time=SignupConfig.conf_code_expiry,
username=signup_form.username,
)
return Response(status_code=204)
else:
# Save the Account into the database
users_collection.insert_one(signup_form.model_dump())
return create_user(signup_form, background_tasks)


@router.post(
"/confirm",
response_model=LoginResponse,
responses={
404: {"description": "No Account Found with this code. Or Code expired."},
200: {"description": "Account was created successfully."},
},
)
async def confirm_email(
payload: ConfirmEmailCodeRequest, background_tasks: BackgroundTasks
):
try:
acc = temp_accounts[payload.email]
if acc["code"] != payload.code:
return Response(status_code=404)
except KeyError:
return Response(status_code=404)
del temp_accounts[payload.email]
# Account is confirmed, create the user
return create_user(acc["form"], background_tasks)
Loading

0 comments on commit 2a8b054

Please sign in to comment.