Skip to content

Commit

Permalink
Added API Token Verification to lock restricted calls (#182)
Browse files Browse the repository at this point in the history
* Added ability to set and clear token headers from front end

* Added bearer token checking to restricted API calls - Protein upload, delete, and edit.

* Linting pass

* fix: print -> log.warn for consistency

---------

Co-authored-by: xnought <bertuccd@oregonstate.edu>
  • Loading branch information
ansengarvin and xnought authored Feb 27, 2024
1 parent 96e679e commit 93ea05d
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 10 deletions.
12 changes: 8 additions & 4 deletions backend/src/api/protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from ..db import Database, bytea_to_str, str_to_bytea

from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody, CamelModel
from ..auth import requiresAuthentication
from io import BytesIO
from fastapi import APIRouter
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.requests import Request

router = APIRouter()

Expand Down Expand Up @@ -179,7 +181,8 @@ def get_protein_entry(protein_name: str):

# TODO: add permissions so only the creator can delete not just anyone
@router.delete("/protein/entry/{protein_name:str}", response_model=None)
def delete_protein_entry(protein_name: str):
def delete_protein_entry(protein_name: str, req: Request):
requiresAuthentication(req)
# Todo, have a meaningful error if the delete fails
with Database() as db:
# remove protein
Expand Down Expand Up @@ -207,7 +210,8 @@ def upload_protein_png(body: UploadPNGBody):

# None return means success
@router.post("/protein/upload", response_model=UploadError | None)
def upload_protein_entry(body: UploadBody):
def upload_protein_entry(body: UploadBody, req: Request):
requiresAuthentication(req)
# check that the name is not already taken in the DB
if protein_name_found(body.name):
return UploadError.NAME_NOT_UNIQUE
Expand Down Expand Up @@ -262,10 +266,10 @@ def upload_protein_entry(body: UploadBody):

# TODO: add more edits, now only does name and content edits
@router.put("/protein/edit", response_model=UploadError | None)
def edit_protein_entry(body: EditBody):
def edit_protein_entry(body: EditBody, req: Request):
# check that the name is not already taken in the DB
# TODO: check if permission so we don't have people overriding other people's names

requiresAuthentication(req)
try:
if body.new_name != body.old_name:
os.rename(pdb_file_name(body.old_name), pdb_file_name(body.new_name))
Expand Down
24 changes: 21 additions & 3 deletions backend/src/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import jwt
from datetime import datetime, timezone, timedelta
from fastapi.requests import Request
from fastapi import HTTPException
import logging as log

# TODO: This method of secret key generation is, obviously, extremely unsafe.
# This needs to be changed.
Expand All @@ -15,13 +18,28 @@ def generateAuthToken(userId, admin):
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")
# Valid token is always is in the form "Bearer [token]", so we need to slice off the "Bearer" portion.
sliced_token = token[7:]
log.warn(sliced_token)
decoded = jwt.decode(sliced_token, secret_key, algorithms="HS256")
log.warn("Valid token")
log.warn(decoded)
return decoded

# If the token is invalid, return None.
except Exception:
except Exception as err:
log.error("Invalid token:", err)
return None


# Use this function with a request if you want.
def requiresAuthentication(req: Request):
userInfo = authenticateToken(req.headers["authorization"])
if not userInfo or not userInfo.get("admin"):
log.error("Unauthorized User")
raise HTTPException(status_code=403, detail="Unauthorized")
else:
log.warn("User authorized.")
13 changes: 13 additions & 0 deletions frontend/src/lib/backend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
export * from "./openapi";
export { DefaultService as Backend } from "./openapi";
import { OpenAPI } from "./openapi";
import Cookies from "js-cookie";

export const BACKEND_URL = "http://localhost:8000";
OpenAPI.BASE = BACKEND_URL;

// Sets the token header to the stored authentication token from the cookie
export function setToken() {
console.log("Setting Token")
OpenAPI.TOKEN = Cookies.get("auth")
}

// Sets the token header to nothing.
export function clearToken() {
console.log("Clearing token.")
OpenAPI.TOKEN = undefined
}
4 changes: 3 additions & 1 deletion frontend/src/routes/Edit.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Backend, UploadError, type ProteinEntry } from "../lib/backend";
import { Backend, UploadError, setToken, type ProteinEntry } from "../lib/backend";
import { Button, Input, Label, Helper, Select } from "flowbite-svelte";
import { navigate } from "svelte-routing";
import { onMount } from "svelte";
Expand Down Expand Up @@ -140,6 +140,7 @@
on:click={async () => {
if (entry) {
try {
setToken()
const err = await Backend.editProteinEntry({
newName: name,
oldName: entry.name,
Expand Down Expand Up @@ -179,6 +180,7 @@
<Button
color="red"
on:click={async () => {
setToken()
await Backend.deleteProteinEntry(urlId);
navigate("/");
}}>Delete Protein Entry</Button
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/routes/Login.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Backend, type LoginResponse } from "../lib/backend";
import { Backend, clearToken, type LoginResponse } from "../lib/backend";
import { Button, Label, Input } from "flowbite-svelte";
import Cookies from "js-cookie";
import { onMount } from "svelte";
Expand All @@ -11,6 +11,7 @@
* We want to do this to avoid bugs, and to let the user log out.
*/
onMount(async () => {
clearToken()
Cookies.remove("auth")
$user.loggedIn = false
$user.username = ""
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/routes/Upload.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Backend, UploadError } from "../lib/backend";
import { Backend, UploadError, setToken } from "../lib/backend";
import {
Fileupload,
Button,
Expand Down Expand Up @@ -108,6 +108,7 @@

const pdbFileStr = await fileToString(file);
try {
setToken()
const err = await Backend.uploadProteinEntry({
name,
description,
Expand Down

0 comments on commit 93ea05d

Please sign in to comment.