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:
@@ -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
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEICSXpqvHczoQX4EOXgZjbXRTKZ8xrZGq5urrf6JbEugKoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEUqghNqolf173mvhnnGTEuTdSvhf9kyV2VeNala0SOgNFfsNrYXx5
|
||||||
|
vSlGFr47XIPLhiCzEhtV93TCsoRE8kmfjQ==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUqghNqolf173mvhnnGTEuTdSvhf9
|
||||||
|
kyV2VeNala0SOgNFfsNrYXx5vSlGFr47XIPLhiCzEhtV93TCsoRE8kmfjQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -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
@@ -2,4 +2,7 @@ fastapi
|
|||||||
psycopg_pool
|
psycopg_pool
|
||||||
uvicorn
|
uvicorn
|
||||||
psycopg[binary]
|
psycopg[binary]
|
||||||
pydantic
|
pydantic
|
||||||
|
confluent_kafka
|
||||||
|
pyjwt
|
||||||
|
cryptography
|
||||||
Reference in New Issue
Block a user