Skip to content

Commit

Permalink
Login Endpoint (#148)
Browse files Browse the repository at this point in the history
* Beginning to add authentication stuff for login. Code is broken right now.

* Authentication token generation seems to be functional, based on a quick online test. Going to see about decoding.

* Changed expiration on token to 24 hours

* Added ability to authenticate tokens. Will be useful for locking endpoints.

* User login route theoretically working. Need to test now.

* Login endpoint appears functional

* Cleaned up users API code

* Added TODO reminder for authentication

* Removed unused imports to appease ruff.

* Ran Ruff linter to make code look better

* Ran ruff linter again
  • Loading branch information
ansengarvin authored Jan 27, 2024
1 parent 7f9cd6d commit 80897e6
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 3 deletions.
2 changes: 1 addition & 1 deletion backend/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ INSERT INTO species(name) VALUES ('unknown');
/*
* Inserts test user into user table
*/
INSERT INTO users(username, email, pword, admin) VALUES ('test', 'garvina@oregonstate.edu', 'password', '1');
INSERT INTO users(username, email, pword, admin) VALUES ('test', 'garvina@oregonstate.edu', '$2b$12$2Z74k3vqzchWB..McZbdUOp4BXd6RGsWcS0atZJfVVheGexvH7i0O', '1');
38 changes: 36 additions & 2 deletions backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ fastapi = "^0.104.0"
psycopg = "^3.1.12"
psycopg-pool = "^3.1.8"
biopython = "^1.81"
PyJWT = "^2.8.0"
passlib = "^1.7.4"


[tool.poetry.group.dev.dependencies]
Expand Down
41 changes: 41 additions & 0 deletions backend/src/api/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from fastapi import APIRouter
import logging as log
from passlib.hash import bcrypt
from ..api_types import LoginBody, LoginError, ResponseToken
from ..db import Database
from ..auth import generateAuthToken

router = APIRouter()


@router.post("/users/login", response_model=ResponseToken | LoginError)
def login(body: LoginBody):
with Database() as db:
try:
email = body.email
password = body.password

query = (
"""SELECT users.pword, users.admin FROM users WHERE users.email = %s;"""
)
entry_sql = db.execute_return(query, [email])

# Returns "incorrect email/password" message if there is no such account.
if entry_sql is None or len(entry_sql) == 0:
return LoginError.INCORRECT

# Grabs the stored hash and admin.
password_hash, admin = entry_sql[0]

# Returns "incorrect email/password" message if password is incorrect.
if not bcrypt.verify(password, password_hash):
return LoginError.INCORRECT

# Generates the token and returns
token = generateAuthToken(email, admin)
return ResponseToken(token=token)

except Exception as e:
log.error(e)
# TODO: Return something better than query error
return LoginError.QUERY_ERROR
16 changes: 16 additions & 0 deletions backend/src/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,19 @@ class EditBody(CamelModel):
new_species_name: str
new_content: str | None = None
new_refs: str | None = None


class LoginBody(CamelModel):
email: str
password: str


class LoginError(str, enum.Enum):
DEBUG_ACCOUNT = "Debug: Account Not Found"
DEBUG_PASSWORD = "Debug: Incorrect password"
INCORRECT = "Invalid Email or Password"
QUERY_ERROR = "QUERY_ERROR"


class ResponseToken(CamelModel):
token: str
27 changes: 27 additions & 0 deletions backend/src/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import jwt
from datetime import datetime, timezone, timedelta

# TODO: This method of secret key generation is, obviously, extremely unsafe.
# This needs to be changed.
secret_key = "SuperSecret"


def generateAuthToken(userId, admin):
payload = {
"email": userId,
"admin": admin,
"exp": datetime.now(tz=timezone.utc) + timedelta(hours=24),
}
return jwt.encode(payload, secret_key, algorithm="HS256")


# TODO: Find out how FastAPI handles middleware functions, and turn this into one.
def authenticateToken(token):
# Return the decoded token if it's valid.
try:
decoded = jwt.decode(token, secret_key, algorithms="HS256")
return decoded

# If the token is invalid, return None.
except Exception:
return None
3 changes: 3 additions & 0 deletions backend/src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
from .db import Database, bytea_to_str, str_to_bytea
from .protein import parse_protein_pdb, pdb_file_name, protein_name_found, pdb_to_fasta
from .setup import disable_cors, init_fastapi_app
from .api import users


app = init_fastapi_app()
disable_cors(app, origins=[os.environ["PUBLIC_FRONTEND_URL"]])

app.include_router(users.router)


@app.get("/pdb/{protein_name:str}")
def get_pdb_file(protein_name: str):
Expand Down

0 comments on commit 80897e6

Please sign in to comment.