Skip to content

Commit

Permalink
Merge pull request #37 from 1toldyou/main
Browse files Browse the repository at this point in the history
alternative login method
  • Loading branch information
1toldyou authored May 26, 2022
2 parents 01cf300 + e6cdffa commit da5ddb3
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 84 deletions.
9 changes: 7 additions & 2 deletions Document/FeatureSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ which including interactive interface with minial amount of explanation about ho
After period of development, while we're getting more functionality, the code are getting messier, so it's time to rewrite some of these code
- Break code into smaller file by utilize the Router API
- New URL scheme that describe the action of this endpoint at the end;
combined public and private endpoint, authenticate based on the token passed-in
combined public and private endpoint, authenticate based on the token passed-in;
if the request modify any existing data then its POST request, GET is read-only
- Performance is the priority now;
using direct native connection to the database achieved resulted unnoticeable API latency
optimized query to reduce overhead
Expand All @@ -76,6 +77,10 @@ Might not a significant feature

### Permanently Death
#### SQL Database
Somewhat complicated to make SQL query for the data-structure I already made but seamlessly works with Document-Base NotOnlySQL database,
plus can immune common SQL-Injection attack


## Developer's notebook
### Authentication
#### GitHub OAuth

53 changes: 27 additions & 26 deletions Document/WorkBreakdownStructure.md

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions example_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,22 @@
"expiration_timestamp_int": 1658293369
}

PASSWORD_INFO = {
LOGIN_INFO_1 = {
"structure_version": 1,
"person_id": "1234567890",
"password_hash": "12B03226A6D8BE9C6E8CD5E55DC6C7920CAAA39DF14AAB92D5E3EA9340D1C8A4D3D0B8E4314F1F6EF131BA4BF1CEB9186AB87C801AF0D5C95B1BEFB8CEDAE2B9",
"password_length": 8,
"password_hash": "12b03226a6d8be9c6e8cd5e55dc6c7920caaa39df14aab92d5e3ea9340d1c8a4d3d0b8e4314f1f6ef131ba4bf1ceb9186ab87c801af0d5c95b1befb8cedae2b9",
"password_length": 10,
}

LOGIN_INFO_2 = {
"structure_version": 2,
"person_id": "1234567890",
"password_hash": "12b03226a6d8be9c6e8cd5e55dc6c7920caaa39df14aab92d5e3ea9340d1c8a4d3d0b8e4314f1f6ef131ba4bf1ceb9186ab87c801af0d5c95b1befb8cedae2b9", # the generated from hashlib is in lowercase
"password_length": 10,
"totp_status": "enabled",
"totp_secret_key": "MWKXM4SZS7O2Q7S5KU5TBJ2INYSH42UQ", # pyotp.random_base32()
"github_oauth_status": "enabled",
"github_email": "example@752628.xyz"
}

# For directly compatible with vanilla JSON, do not add comma after each last item
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ slowapi
starlette
python-multipart
captcha
pymongo
pymongo
dnspython
pyotp
aiohttp
217 changes: 170 additions & 47 deletions route/v2_auth.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
# Builtin library
from datetime import datetime
import hashlib
import aiohttp

# Framework core library
from starlette.requests import Request
from fastapi import APIRouter, Header
from fastapi.responses import JSONResponse
import pyotp

# Local file
from util import random_content, json_body
from util import json_body, token_tool
import util.pymongo_wrapper as DocumentDB

router = APIRouter()


@router.post("/token/generate", tags=["V2"])
async def v2_generate_auth_token(request: Request, cred: json_body.PasswordLoginBody):
@router.post("/token/revoke", tags=["V2"])
async def v2_revoke_auth_token(request: Request, pa_token: str = Header(None)):
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
token_deletion_query = DocumentDB.delete_one(
collection="TokenV3",
find_filter={"token_value": pa_token},
db_client=db_client)
mongo_client.close()
if token_deletion_query is None:
return JSONResponse(status_code=500, content={"status": "failed to remove the old token to database", "pa_token": pa_token})
elif token_deletion_query.deleted_count == 0:
return JSONResponse(status_code=404, content={"status": "token not found", "pa_token": pa_token})
elif token_deletion_query.deleted_count == 1:
return JSONResponse(status_code=200, content={"status": "deleted", "pa_token": pa_token})


@router.post("/password/verify", tags=["V2"])
async def v2_verify_auth_password(request: Request, cred: json_body.PasswordLoginBody):
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
credential_verify_query = DocumentDB.find_one(collection="LoginV1",
Expand All @@ -30,51 +48,12 @@ async def v2_generate_auth_token(request: Request, cred: json_body.PasswordLogin
content={"status": "not found or not match",
"person_id": cred.person_id,
"password": cred.password})
while True:
# Checking if the same token already being use
# There is no do-while loop in Python
generated_token = random_content.generate_access_token()
current_checking_query = DocumentDB.find_one(collection="TokenV1",
find_filter={"token_value": generated_token},
db_client=db_client)
if current_checking_query is None:
break
create_at = int(datetime.now().timestamp())
expire_at = create_at + cred.token_lifespan
token_record_query = DocumentDB.insert_one(
collection="TokenV3",
document_body={
"structure_version": 3,
"person_id": cred.person_id,
"token_value": generated_token,
"token_hash": hashlib.sha512(generated_token.encode("utf-8")).hexdigest(),
"creation_timestamp_int": create_at,
"expiration_timestamp_int": expire_at
},
db_client=db_client)
if token_record_query is None:
return JSONResponse(status_code=500,
content={"status": "token generated but failed to insert that token to database"})
generated_token = token_tool.generate_pa_token_and_record(db_client=db_client,
person_id=cred.person_id,
token_lifespan=cred.token_lifespan)
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "success", "pa_token": generated_token, "expiration_timestamp": expire_at})


@router.post("/token/revoke", tags=["V2"])
async def v2_revoke_auth_token(request: Request, pa_token: str = Header(None)):
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
token_deletion_query = DocumentDB.delete_one(
collection="TokenV3",
find_filter={"token_value": pa_token},
db_client=db_client)
mongo_client.close()
if token_deletion_query is None:
return JSONResponse(status_code=500, content={"status": "failed to remove the old token to database", "pa_token": pa_token})
elif token_deletion_query.deleted_count == 0:
return JSONResponse(status_code=404, content={"status": "token not found", "pa_token": pa_token})
elif token_deletion_query.deleted_count == 1:
return JSONResponse(status_code=200, content={"status": "deleted", "pa_token": pa_token})
content={"status": "success", "pa_token": generated_token[0], "expiration_timestamp": generated_token[1]})


# TODO: revoke existing session/token
Expand Down Expand Up @@ -112,3 +91,147 @@ async def v2_update_auth_password(request: Request, old_cred: json_body.Password
"password": old_cred.password})
mongo_client.close()
return JSONResponse(status_code=200, content={"status": "success", "voided": old_cred.password})


@router.post("/totp/enable", tags=["V2"])
async def v2_enable_auth_totp(request: Request, cred: json_body.PasswordLoginBody):
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
# same as the traditional plain-password login
credential_verify_query = DocumentDB.find_one(collection="LoginV2",
find_filter={"person_id": cred.person_id},
db_client=db_client)
print(credential_verify_query)
if (credential_verify_query is None) \
or (hashlib.sha512(cred.password.encode("utf-8")).hexdigest() != credential_verify_query["password_hash"]):
mongo_client.close()
return JSONResponse(status_code=403,
content={"status": "user not found or not match",
"person_id": cred.person_id,
"password": cred.password})
if credential_verify_query["totp_status"] != "disabled":
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "Time-based OTP already enabled for this user",
"person_id": cred.person_id})
new_secret_key = pyotp.random_base32()
authenticator_url = pyotp.totp.TOTP(new_secret_key).provisioning_uri(name=cred.person_id,
issuer_name='Plan-At')
credential_modify_query = DocumentDB.update_one(db_client=db_client,
collection="LoginV2",
find_filter={"person_id": cred.person_id},
changes={"$set": {"totp_status": "enabled",
"totp_secret_key": new_secret_key}})
if credential_modify_query.matched_count != 1 and credential_modify_query.modified_count != 1:
return JSONResponse(status_code=500, content={"status": "failed to register the secret_key for totp in database",
"matched_count": credential_modify_query.matched_count,
"modified_count": credential_modify_query.modified_count})
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "Time-based OTP enabled for this user",
"person_id": cred.person_id,
"authenticator_url": authenticator_url})


@router.post("/totp/disable", tags=["V2"])
async def v2_disable_auth_totp(request: Request, cred: json_body.PasswordLoginBody):
# Copy and Paste of /enable
# Not require current output from Authenticator since the user might lose their
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
# same as the traditional plain-password login
credential_verify_query = DocumentDB.find_one(collection="LoginV2",
find_filter={"person_id": cred.person_id},
db_client=db_client)
print(credential_verify_query)
if (credential_verify_query is None) \
or (hashlib.sha512(cred.password.encode("utf-8")).hexdigest() != credential_verify_query["password_hash"]):
mongo_client.close()
return JSONResponse(status_code=403,
content={"status": "user not found or not match",
"person_id": cred.person_id,
"password": cred.password})
# checking current totp status
if credential_verify_query["totp_status"] != "enabled":
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "Time-based OTP not enabled for this user",
"person_id": cred.person_id})
credential_modify_query = DocumentDB.update_one(db_client=db_client,
collection="LoginV2",
find_filter={"person_id": cred.person_id},
changes={"$set": {"totp_status": "disabled",
"totp_secret_key": ""}})
if credential_modify_query.matched_count != 1 and credential_modify_query.modified_count != 1:
return JSONResponse(status_code=500, content={"status": "failed to delete existing secret_key for totp in database",
"matched_count": credential_modify_query.matched_count,
"modified_count": credential_modify_query.modified_count})
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "Time-based OTP disabled for this user",
"person_id": cred.person_id})


@router.post("/totp/verify", tags=["V2"])
async def v2_verify_auth_totp(request: Request, person_id: str, totp_code: str):
# Copy and Paste of /disable
# Not require current output from Authenticator since the user might lose their
mongo_client = DocumentDB.get_client()
db_client = mongo_client.get_database(DocumentDB.DB)
# if len(str(int(totp_code))) != 6: # also verify if its actual int but not working if start with zero
if len(totp_code) != 6: # also verify if its actual int but not working if start with zero
return JSONResponse(status_code=400,
content={"status": "totp_code malformed",
"totp_code": totp_code})
# same as the traditional plain-password login
credential_verify_query = DocumentDB.find_one(collection="LoginV2",
find_filter={"person_id": person_id},
db_client=db_client)
print(credential_verify_query)
if credential_verify_query is None:
mongo_client.close()
return JSONResponse(status_code=403,
content={"status": "user not found or totp_code not match",
"person_id": person_id,
"totp_code": totp_code})
# Checking current totp status
if credential_verify_query["totp_status"] != "enabled":
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "Time-based OTP not enabled for this user",
"person_id": person_id})

if not pyotp.TOTP(credential_verify_query["totp_secret_key"]).verify(totp_code):
mongo_client.close()
return JSONResponse(status_code=403,
content={"status": "user not found or totp_code not match",
"person_id": person_id,
"totp_code": totp_code})
generated_token = token_tool.generate_pa_token_and_record(db_client=db_client,
person_id=person_id,
token_lifespan=(60 * 60 * 24 * 1))
mongo_client.close()
return JSONResponse(status_code=200,
content={"status": "success",
"person_id": person_id,
"pa_token": generated_token[0],
"expiration_timestamp": generated_token[1]})


@router.post("/github/enable", tags=["V2"])
async def v2_enable_auth_github(request: Request, req_body: json_body.GitHubOAuthCode, pa_token: str = Header(None)):
github_session = aiohttp.ClientSession()
a = await github_session.post(f"https://github.com/login/oauth/access_token?client_id={1}&client_secret={2}&code={3}")
print(a.status, a.text())
a = a.json()
return JSONResponse(status_code=200, content={"status": "success", "code": req_body.code})


@router.post("/github/disable", tags=["V2"])
async def v2_disable_auth_github(request: Request, req_body: json_body.GitHubOAuthCode, pa_token: str = Header(None)):
pass


@router.post("/github/verify", tags=["V2"])
async def v2_verify_auth_github(request: Request, req_body: json_body.GitHubOAuthCode):
pass
8 changes: 6 additions & 2 deletions route/v2_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,16 @@ async def v2_create_user(request: Request, user_profile: json_body.UserProfileOb
document_body={"structure_version": 1, "person_id": person_id, "event_id_list": []})
print(calendar_index_insert_query.inserted_id)
login_credential_insert_query = DocumentDB.insert_one(db_client=db_client,
collection="LoginV1",
collection="LoginV2",
document_body={
"structure_version": 1,
"structure_version": 2,
"person_id": person_id,
"password_hash": hashlib.sha512(password.encode("utf-8")).hexdigest(),
"password_length": len(password),
"totp_status": "disabled",
"totp_secret_key": "",
"github_oauth_status": "disabled",
"github_email": ""
})
print(login_credential_insert_query.inserted_id)
mongo_client.close()
Expand Down
32 changes: 31 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
from starlette.requests import Request
from starlette.responses import Response, RedirectResponse
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.openapi.utils import get_openapi
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

from constant import ServerConfig, RateLimitConfig, MediaAssets, START_TIME, PROGRAM_HASH
from route import fake, v1, v2, v2_captcha, v2_auth, v2_user, v2_calendar, v2_hosting
from util import docs_page

app = FastAPI()

Expand All @@ -32,6 +34,15 @@
app.include_router(fake.router, prefix="/fake")
app.include_router(v1.router)

"""enable this for local development or where have no nginx presence"""
# app.add_middleware(
# CORSMiddleware,
# allow_origins="*",
# allow_credentials=True,
# allow_methods=["*"],
# allow_headers=["*"],
# )


@app.middleware("http")
async def log_requests(request: Request, call_next):
Expand Down Expand Up @@ -89,6 +100,11 @@ def get_favicon(request: Request):
return RedirectResponse(url=MediaAssets.FAVICON)


@app.get("/doc", include_in_schema=False)
def overridden_swagger():
return HTMLResponse(status_code=200, content=docs_page.HTML)


@app.get("/docs", include_in_schema=False)
def overridden_swagger():
return get_swagger_ui_html(openapi_url="/openapi.json", title="Plan-At", swagger_favicon_url=MediaAssets.FAVICON)
Expand Down Expand Up @@ -156,6 +172,20 @@ def api_tool_delay(request: Request, sleep_time: int):
return JSONResponse(status_code=200, content={"status": "finished"})


@app.get("/everything")
async def receive_everything(request: Request):
print(request.headers)
print(await request.body())
return JSONResponse(status_code=200, content={"status": "finished"})


@app.post("/everything")
async def receive_everything(request: Request):
print(request.headers)
print(await request.body())
return JSONResponse(status_code=200, content={"status": "finished"})


if __name__ == "__main__":
if sys.platform == "win32":
uvicorn.run("server:app", debug=True, reload=True, port=ServerConfig.PORT, host=ServerConfig.HOST,
Expand Down
Loading

0 comments on commit da5ddb3

Please sign in to comment.