Basic JWT endpoints

This commit is contained in:
Darryl Nixon 2023-05-23 17:14:49 -07:00
parent 7c5919f073
commit 1fa3d8a372
13 changed files with 180 additions and 27 deletions

22
.env
View file

@ -1,3 +1,12 @@
# DATABASE_* variables are used both to initially configure the
# ghostforge postgresql database and to later access the data from
# ghostforge execution.
DATABASE_CONTAINER_NAME=ghostforge-db
DATABASE_USER=ghost
DATABASE_NAME=ghostforge
DATABASE_PORT=5432
DATABASE_PASSWORD=
# GHOSTFORGE_*_WEB_PORT variables are used to determine what # GHOSTFORGE_*_WEB_PORT variables are used to determine what
# port the web interface is served on within the container (INTERNAL) # port the web interface is served on within the container (INTERNAL)
# and what port this will map to on the host (HOST). # and what port this will map to on the host (HOST).
@ -11,8 +20,11 @@ GHOSTFORGE_INTERNAL_WEB_PORT=1337
# Valid values are [prod, dev] # Valid values are [prod, dev]
GHOSTFORGE_ENV=prod GHOSTFORGE_ENV=prod
# DATABASE_* variables are used both to initially configure the # GHOSTFORGE_DATA_DIR stores persistent files for a ghostforge instance
# ghostforge postgresql database and to later access the data from # and should be mapped or stored as a volume from the docker host.
# ghostforge execution. # GHOSTFORGE_DATA_DIR is created within the ghostforge container.
DATABASE_USER=ghost GHOSTFORGE_DATA_DIR=/data
DATABASE_NAME=ghostforge
# JWT_SECRET is used for authentication purposes and should be
# randomized using the method in README.md.
GHOSTFORGE_JWT_SECRET=

View file

@ -1,3 +1,3 @@
[flake8] [flake8]
max-line-length = 160 max-line-length = 160
exclude = docs/*, .git, __pycache__ exclude = docs/*, .git, __pycache__, build

View file

@ -2,12 +2,13 @@ FROM python:3.11-alpine
ENV DATABASE_PASSWORD "" ENV DATABASE_PASSWORD ""
RUN if [ -z "${DATABASE_PASSWORD}" ]; then echo "ghostforge build error: Set DATABASE_PASSWORD in .env."; exit 1; fi RUN if [ -z "${DATABASE_PASSWORD}" ]; then echo "ghostforge build error: Set DATABASE_PASSWORD in .env."; exit 1; fi
RUN if [ -z "${GHOSTFORGE_JWT_SECRET}" ]; then echo "ghostforge build error: Set GHOSTFORGE_JWT_SECRET in .env."; exit 1; fi
WORKDIR /ghostforge WORKDIR /ghostforge
COPY . . COPY . .
RUN rm .env RUN rm .env
RUN mkdir -p "${GHOSTFORGE_DATA_DIR}"
RUN pip install --no-cache-dir --requirement requirements.txt
RUN pip install . RUN pip install .
ENV GHOSTFORGE_INTERNAL_WEB_PORT=8080 ENV GHOSTFORGE_INTERNAL_WEB_PORT=8080

View file

@ -23,7 +23,8 @@ You'll need `docker-compose` installed or you can convert the contents of `docke
```bash ```bash
git clone https://github.com/darrylnixon/ghostforge.git && \ git clone https://github.com/darrylnixon/ghostforge.git && \
cd ghostforge && \ cd ghostforge && \
PW=$(/usr/bin/env python3 -c "import secrets; print(secrets.token_urlsafe(24))") | sed -i .bak "s/^DATABASE_PASSWORD=.*/DATABASE_PASSWORD=$PW" .env; PW=$(/usr/bin/env python3 -c "import secrets; print(secrets.token_urlsafe(32))") /bin/bash -c 'sed -i "" "s/^DATABASE_PASSWORD=.*/DATABASE_PASSWORD=$PW/" .env' && \
JWT=$(/usr/bin/env python3 -c "import secrets; print(secrets.token_urlsafe(32))") /bin/bash -c 'sed -i "" "s/^GHOSTFORGE_JWT_SECRET=.*/GHOSTFORGE_JWT_SECRET=$JWT/" .env';
docker-compose up --detach --build; docker-compose up --detach --build;
docker exec --interactive --tty ghostforge ghostforge_adduser; docker exec --interactive --tty ghostforge ghostforge_adduser;
``` ```

View file

@ -1,9 +0,0 @@
"""
Initialization constants for ghostforge.
Constants:
_PROJECT (str): Name for the project
__version__ (str): The current version of the binhop package
"""
_PROJECT = "ghostforge"
__version__ = "0.0.1"

35
ghostforge/api.py Normal file
View file

@ -0,0 +1,35 @@
from fastapi import Body
from fastapi import Depends
from fastapi import FastAPI
from ghostforge.auth import check_user
from ghostforge.auth.bearer import JWTBearer
from ghostforge.auth.handler import signJWT
from ghostforge.models import UserLoginSchema
# define("port", default=os.environ.get("GHOSTFORGE_INTERNAL_WEB_PORT", 1337), help="Webserver port", type=int)
# define("db_host", default=os.environ.get("DATABASE_CONTAINER_NAME", "localhost"), help="Host/IP of db")
# define("db_port", default=os.environ.get("DATABASE_PORT", 5432), help="Port of db", type=int)
# define("db_database", default=os.environ.get("DATABASE_NAME", "ghostforge"), help="Name of db")
# define("db_user", default=os.environ.get("DATABASE_USER", "ghostforge"), help="User with access to db")
# define("db_password", default=os.environ.get("DATABASE_PASSWORD", "secure"), help="Password for db user")
# define("secret_key", default=os.environ.get("DATABASE_PASSWORD", "secure"), help="Password for db user")
app = FastAPI()
@app.get("/", tags=["root"])
async def read_root() -> dict:
return {"message": "Welcome!"}
@app.post("/login", tags=["user"])
async def user_login(user: UserLoginSchema = Body(...)):
if check_user(user):
return signJWT(user.name)
return {"error": "Wrong login details!"}
@app.post("/auth_check", dependencies=[Depends(JWTBearer())], tags=["auth_check"])
async def auth_check(post) -> dict:
return {"data": "post added."}

View file

@ -0,0 +1,11 @@
from ghostforge.models import UserLoginSchema
users = [{"name": "test", "password": "pw"}]
def check_user(data: UserLoginSchema):
print(data)
for user in users:
if user["name"] == data.name and user["password"] == data.password:
return True
return False

35
ghostforge/auth/bearer.py Normal file
View file

@ -0,0 +1,35 @@
from fastapi import HTTPException
from fastapi import Request
from fastapi.security import HTTPAuthorizationCredentials
from fastapi.security import HTTPBearer
from ghostforge.auth.handler import decodeJWT
# from https://github.com/testdrivenio/fastapi-jwt/
class JWTBearer(HTTPBearer):
def __init__(self, auto_error: bool = True):
super(JWTBearer, self).__init__(auto_error=auto_error)
async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(status_code=403, detail="Invalid authentication scheme.")
if not self.verify_jwt(credentials.credentials):
raise HTTPException(status_code=403, detail="Invalid token or expired token.")
return credentials.credentials
else:
raise HTTPException(status_code=403, detail="Invalid authorization code.")
def verify_jwt(self, jwtoken: str) -> bool:
isTokenValid: bool = False
try:
payload = decodeJWT(jwtoken)
except Exception:
payload = None
if payload:
isTokenValid = True
return isTokenValid

View file

@ -0,0 +1,28 @@
import os
import time
from typing import Dict
import jwt
# Based on handler from https://github.com/testdrivenio/fastapi-jwt/
JWT_SECRET = os.environ.get("GHOSTFORGE_JWT_SECRET")
def token_response(token: str):
return {"access_token": token}
def signJWT(user: str) -> Dict[str, str]:
payload = {"user_id": user, "expires": time.time() + 600}
token = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
return token_response(token)
def decodeJWT(token: str) -> dict:
try:
decoded_token = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
return decoded_token if decoded_token["expires"] >= time.time() else None
except Exception:
return {}

View file

@ -0,0 +1,9 @@
import os
import uvicorn
def start_api() -> None:
uvicorn.run(
"ghostforge.api:app", host="0.0.0.0", port=os.environ.get("GHOSTFORGE_INTERNAL_WEB_PORT", 1337), reload=True
)

27
ghostforge/models.py Normal file
View file

@ -0,0 +1,27 @@
import datetime
from pydantic import BaseModel
from pydantic import Field
class UserSchema(BaseModel):
name: str = Field(...)
password: str = Field(...)
created: datetime.datetime = datetime.datetime
class Config:
schema_extra = {
"example": {
"name": "Jeremy Tootsieroll",
"password": "notarealpassword",
"created": "2021-03-05T08:21:00.000Z",
}
}
class UserLoginSchema(BaseModel):
name: str = Field(...)
password: str = Field(...)
class Config:
schema_extra = {"example": {"name": "Jeremy Tootsieroll", "password": "notarealpassword"}}

View file

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools>=61.0"] requires = ["setuptools>=67.8"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@ -9,22 +9,26 @@ authors = [{ name = "ghostforge", email = "git@nixon.mozmail.com" }]
description = "A false identity information manager for privacy prudent persons" description = "A false identity information manager for privacy prudent persons"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = ["tornado", "queries"] license = { text = "MIT" }
license = "MIT" dependencies = [
"fastapi>=0.95.2",
"uvicorn>=0.22.0",
"loguru>=0.7.0",
"passlib>=1.7.4",
"pydantic>=1.10.8",
]
[project.scripts] [project.scripts]
ghostforge_serve = "ghostforge.cli:service" ghostforge_serve = "ghostforge.cli:start_api"
[project.urls] [project.urls]
"Homepage" = "https://github.com/DarrylNixon/ghostforge" "Homepage" = "https://github.com/DarrylNixon/ghostforge"
"Bug Tracker" = "https://github.com/DarrylNixon/ghostforge/issues" "Bug Tracker" = "https://github.com/DarrylNixon/ghostforge/issues"
[tool.bandit] [tool.bandit]
exclude_dirs = ["/doc"] exclude_dirs = ["/doc", "/build"]
skips = [] # TODO: Stop skipping B104 (binding on 0.0.0.0), is there a nice way to get a good docker bind address?
skips = ["B104"]
[tool.black] [tool.black]
line-length = 120 line-length = 120
[tool.isort]
profile = "black"

View file

@ -1 +0,0 @@
.