geändert: Makefile

geändert:       docker-compose.yml
	neue Datei:     keys/ec_private.pem
	neue Datei:     keys/ec_public.pem
	geändert:       main.py
	geändert:       requirements.txt
This commit is contained in:
2025-12-21 17:39:32 +01:00
parent c571c0cbb2
commit 70dbf0b120
6 changed files with 272 additions and 42 deletions
+3 -3
View File
@@ -2,9 +2,9 @@ build:
podman-compose build podman-compose build
reload: reload:
podman rm authentication_app_1 podman rm backend_authentication_app_1
podman rm authentication_db_1 podman rm backend_authentication_db_1
podman rm authentication_migration_1 podman rm backend_authentication_migration_1
up: up:
podman-compose up --build podman-compose up --build
+3 -3
View File
@@ -18,10 +18,10 @@ services:
- .env - .env
db: db:
image: postgres:15 image: docker.io/library/postgres:15
environment: environment:
- DB_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - DB_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
env_file: env_file:
- .env - .env
volumes: # volumes:
- /srv/data/db/services/authentication:/var/lib/postgresql/data # - /srv/data/db/services/authentication:/var/lib/postgresql/data
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICSXpqvHczoQX4EOXgZjbXRTKZ8xrZGq5urrf6JbEugKoAoGCCqGSM49
AwEHoUQDQgAEUqghNqolf173mvhnnGTEuTdSvhf9kyV2VeNala0SOgNFfsNrYXx5
vSlGFr47XIPLhiCzEhtV93TCsoRE8kmfjQ==
-----END EC PRIVATE KEY-----
+4
View File
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUqghNqolf173mvhnnGTEuTdSvhf9
kyV2VeNala0SOgNFfsNrYXx5vSlGFr47XIPLhiCzEhtV93TCsoRE8kmfjQ==
-----END PUBLIC KEY-----
+253 -35
View File
@@ -1,20 +1,131 @@
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends, HTTPException, Response, Request, status, Cookie
from psycopg_pool import AsyncConnectionPool from psycopg_pool import AsyncConnectionPool
import psycopg
import uvicorn import uvicorn
import os import os
from pydantic import BaseModel from pydantic import BaseModel, Field, model_validator, field_validator
import time
from datetime import date, datetime, timedelta, timezone
from confluent_kafka import Producer
import uuid
import hashlib
import jwt
class Item(BaseModel): # class Item(BaseModel):
name: str # name: str
description: str # description: str
reporter: str # reporter: str
priority: int # priority: int
is_stupid: bool # is_stupid: bool
app = FastAPI()
with open('keys/ec_private.pem', 'rb') as f:
PRIVATE_KEY = f.read()
with open('keys/ec_public.pem', 'rb') as f:
PUBLIC_KEY = f.read()
REGISTRATION_MINIMUM_AGE = 4745
def get_month_length(month, year = time.localtime().tm_year):
assert not (month > 12 or month < 1)
return {
1: 31,
2: 29 if year % 4 == 0 else 28,
3: 31,
4: 30,
5: 31,
6: 30,
7: 31,
8: 31,
9: 30,
10: 31,
11: 30,
12: 31
}[month]
class User(BaseModel):
email: str = Field(..., max_length = 64)
password: str
birthYear: int
birthMonth: int = Field(..., ge=1, le=12)
birthDay: int
firstName: str = Field(..., max_length = 64)
lastName: str = Field(..., max_length = 64)
preInstalledApplications: dict
@model_validator(mode='before')
def strip_all(cls, values):
return {k: v.strip() if type(v) and k != 'password' == str else v for k, v in values.items()}
@field_validator('email')
def validate_email(cls, value):
def e(): raise ValueError('invalid e-mail address')
if not '@' in value or ' ' in value:
e()
user, host = value.split('@')
if host.endswith('.'):
host = host[:-1]
if not user or not host or '.' not in host or host.startswith('.'):
e()
labels = host.split('.')
for l in labels:
if len(l.encode()) < 1 or len(l.encode()) > 63:
e()
return value
@field_validator('password')
def validate_password_security(cls, password):
password = password
number_of_caps = 0
number_of_smalls = 0
number_of_numbers = 0
number_of_specials = 0
for char in password:
if char.isalnum():
if char.isalpha():
if char.capitalize() == char:
number_of_caps += 1
else:
number_of_smalls += 1
else:
number_of_numbers += 1
else:
number_of_specials += 1
if not (number_of_caps > 0 and number_of_numbers > 0 and number_of_smalls > 0 and number_of_specials > 0 and len(password) > 7):
raise ValueError('the password does not match the requirements')
return password
@model_validator(mode='before')
def validate_pre_installed_applications(cls, values):
...
return values
@model_validator(mode='before')
def validate_birth_date(cls, values):
day = values.get('birthDay')
if day < 1 or day > get_month_length(values.get('birthMonth'), values.get('birthYear')):
raise ValueError('invalid birth day')
today = date.today()
birth_date = date(values.get('birthYear'), values.get('birthMonth'), day)
if not (today >= birth_date):
raise ValueError('the date cannot be in future')
if (today - birth_date).days < REGISTRATION_MINIMUM_AGE:
raise ValueError('age restriction, minimum age is 13 years')
return values
class Session(BaseModel):
password: str
stayLoggedIn: bool = Field(False)
app = FastAPI(
# docs_url = None,
# redoc_url = None,
# openapi_url = None
)
pool: AsyncConnectionPool pool: AsyncConnectionPool
@app.on_event('startup') @app.on_event('startup')
async def on_startup(): async def on_startup():
global pool global pool
@@ -28,7 +139,8 @@ async def on_startup():
async with pool.connection() as conn: async with pool.connection() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute('CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(64), remail VARCHAR(64), phone VARCHAR(16), password VARCHAR(128))') await cursor.execute('CREATE TABLE IF NOT EXISTS users (id UUID PRIMARY KEY, email VARCHAR(64) UNIQUE, phone VARCHAR(32), password BYTEA)')
await cursor.execute('CREATE TABLE IF NOT EXISTS recovery (id UUID PRIMARY KEY, remail VARCHAR(64))')
print('[INFO] Database initialized') print('[INFO] Database initialized')
await conn.commit() await conn.commit()
@@ -41,38 +153,144 @@ async def get_conn():
async with pool.connection() as conn: async with pool.connection() as conn:
yield conn yield conn
@app.get('/') @app.post('/users')
def read_root(): async def create_user(user: User, conn = Depends(get_conn)):
'''Register a user'''
async with conn.cursor() as cursor:
user_id = uuid.uuid4()
print(await (await cursor.execute('SELECT * FROM users')).fetchall())
print(await (await cursor.execute('SELECT * FROM recovery')).fetchall())
try:
await cursor.execute('INSERT INTO users (id, email, phone, password) VALUES (%s, %s, %s, %s)', (user_id, user.email, None, hashlib.sha3_512(user.password.encode()).digest()))
await cursor.execute('INSERT INTO recovery (id, remail) VALUES (%s, %s)', (user_id, None))
except psycopg.errors.UniqueViolation:
raise HTTPException(status_code=409, detail = 'this user is already registered')
conn.commit()
return { return {
'message': 'microservice is running' 'id': user_id,
'email': user.email,
'remail': None,
'phone': None
} }
@app.post('/item') @app.post('/users/{user}/session')
async def create_item(item: Item, conn = Depends(get_conn)): async def create_session(response: Response, session: Session, user: uuid.UUID, conn = Depends(get_conn)):
'''Create a session'''
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute('INSERT INTO items (name, description, reporter, priority, is_stupid) VALUES (%s, %s, %s, %s, %s) RETURNING *', (item.name, item.description, item.reporter, item.priority, item.is_stupid)) await cursor.execute('SELECT COUNT(*) FROM users WHERE id = %s', (user,))
i = await cursor.fetchone() if not (await cursor.fetchone())[0]:
return {'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]} raise HTTPException(status_code = 404, detail = 'user not found')
await cursor.execute('SELECT password FROM users WHERE id = %s', (user,))
if hashlib.sha3_512(session.password.encode()).digest() != (await cursor.fetchone())[0]:
raise HTTPException(status_code = 401, detail = 'Unauthorized')
payload = {
'iss': 'http://localhost:1234',
'sub': str(user),
'aud': 'jcloud-auth',
'iat': datetime.now(tz=timezone.utc),
'exp': datetime.now(tz=timezone.utc) + timedelta(days=20),
}
token = jwt.encode(
payload,
PRIVATE_KEY,
algorithm = 'ES256',
# headers = {
# 'kid': 'ec-key-2025-01'
# }
)
response.set_cookie(
key = 'auth_token',
value = token,
max_age = 1728000 if session.stayLoggedIn else None,
httponly = True,
secure = False,
samesite = 'strict'
)
response.set_cookie(
key = 'user',
value = str(user),
max_age = 1728000 if session.stayLoggedIn else None,
httponly = True,
secure = False,
samesite = 'strict',
)
response.set_cookie
return {
'user': user,
'authToken': token,
'exp': datetime.now(tz=timezone.utc) + timedelta(days=20)
}
@app.get('/item/{item_id}') @app.delete('/users/session', status_code = status.HTTP_204_NO_CONTENT)
async def read_item(item_id, conn = Depends(get_conn)): async def delete_session(response: Response):
'''Delete a session'''
response.set_cookie(
key = 'auth_token',
value = '',
max_age = -1,
httponly = True,
secure = False
)
response.set_cookie(
key = 'user',
value = '',
max_age = -1,
httponly = True,
secure = False
)
@app.get('/users/{user}/validity')
async def validate_token(user: uuid.UUID, conn = Depends(get_conn), Auth: str = Cookie(...)):
'''Validate a token'''
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute('SELECT * FROM items WHERE id = %s', (item_id,)) await cursor.execute('SELECT COUNT(*) FROM users WHERE id = %s', (user,))
i = await cursor.fetchone() if not (await cursor.fetchone())[0]:
return {'id': i[0], 'name' : i[1], 'description': i[2], 'reporter': i[3], 'priority': i[4], 'is_stupid': i[5]} raise HTTPException(status_code = 404, detail = 'User not found')
try:
jwt.decode(
Auth,
PUBLIC_KEY,
algorithms = ['ES256'],
audience = 'jcloud-auth',
issuer = 'http://localhost:1234',
subject = str(user)
)
except:
raise HTTPException(status_code = 401, detail = 'Unauthorized')
return b'success'
@app.get('/items') @app.get('/health')
async def read_item(conn = Depends(get_conn)): async def get_health_status():
async with conn.cursor() as cursor: '''Sends `OK`'''
await cursor.execute('SELECT * FROM items') return b'OK'
items = await cursor.fetchall()
return [{'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]} for i in items]
@app.get('/datadir') # @app.post('/item')
async def get_datadir(conn = Depends(get_conn)): # async def create_item(item: Item, conn = Depends(get_conn)):
async with conn.cursor() as cursor: # async with conn.cursor() as cursor:
await cursor.execute('SHOW data_directory') # await cursor.execute('INSERT INTO items (name, description, reporter, priority, is_stupid) VALUES (%s, %s, %s, %s, %s) RETURNING *', (item.name, item.description, item.reporter, item.priority, item.is_stupid))
return {'res': await cursor.fetchall()} # i = await cursor.fetchone()
# return {'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]}
# @app.get('/item/{item_id}')
# async def read_item(item_id, conn = Depends(get_conn)):
# async with conn.cursor() as cursor:
# await cursor.execute('SELECT * FROM items WHERE id = %s', (item_id,))
# i = await cursor.fetchone()
# return {'id': i[0], 'name' : i[1], 'description': i[2], 'reporter': i[3], 'priority': i[4], 'is_stupid': i[5]}
# @app.get('/items')
# async def read_item(conn = Depends(get_conn)):
# async with conn.cursor() as cursor:
# await cursor.execute('SELECT * FROM items')
# items = await cursor.fetchall()
# return [{'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]} for i in items]
# @app.get('/datadir')
# async def get_datadir(conn = Depends(get_conn)):
# async with conn.cursor() as cursor:
# await cursor.execute('SHOW data_directory')
# return {'res': await cursor.fetchall()}
if __name__ == '__main__': if __name__ == '__main__':
uvicorn.run( uvicorn.run(
+4 -1
View File
@@ -2,4 +2,7 @@ fastapi
psycopg_pool psycopg_pool
uvicorn uvicorn
psycopg[binary] psycopg[binary]
pydantic pydantic
confluent_kafka
pyjwt
cryptography