mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Basic JWT endpoints
This commit is contained in:
parent
7c5919f073
commit
1fa3d8a372
13 changed files with 180 additions and 27 deletions
22
.env
22
.env
|
@ -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=
|
||||||
|
|
2
.flake8
2
.flake8
|
@ -1,3 +1,3 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 160
|
max-line-length = 160
|
||||||
exclude = docs/*, .git, __pycache__
|
exclude = docs/*, .git, __pycache__, build
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
35
ghostforge/api.py
Normal 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."}
|
11
ghostforge/auth/__init__.py
Normal file
11
ghostforge/auth/__init__.py
Normal 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
35
ghostforge/auth/bearer.py
Normal 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
|
28
ghostforge/auth/handler.py
Normal file
28
ghostforge/auth/handler.py
Normal 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 {}
|
|
@ -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
27
ghostforge/models.py
Normal 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"}}
|
|
@ -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"
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
.
|
|
Loading…
Reference in a new issue