Mainly auth re-thinking - just in thought

This commit is contained in:
Parker M. 2024-11-08 23:07:20 -06:00
parent 3cde652d52
commit 8941213c8d
Signed by: parker
GPG Key ID: 505ED36FC12B5D5E
7 changed files with 130 additions and 78 deletions

View File

@ -1,22 +0,0 @@
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
function isJwtExpired (token) {
var jwt = parseJwt(token);
return jwt.exp < Date.now() / 1000;
}
async function refreshAccessToken (refreshToken) {
const data = await fetch('/api/refresh', {
method: 'POST',
headers: {'Authorization': 'Bearer ' + refreshToken}
});
return data.access_token;
}

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI, Depends, Request, Path
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, JSONResponse from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from app.routes.auth_routes import router as auth_router from app.routes.auth_routes import router as auth_router
from app.routes.links_routes import router as links_router from app.routes.links_routes import router as links_router
from app.routes.user_routes import router as user_router from app.routes.user_routes import router as user_router
@ -35,6 +36,7 @@ app.add_middleware(
allow_credentials=True, allow_credentials=True,
) )
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
# Import routes # Import routes

View File

@ -33,7 +33,7 @@ async def login_for_access_token(
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires = timedelta(minutes=15) access_token_expires = timedelta(minutes=1)
access_token = create_access_token( access_token = create_access_token(
data={"sub": user.id, "username": user.username, "refresh": False}, data={"sub": user.id, "username": user.username, "refresh": False},
expires_delta=access_token_expires, expires_delta=access_token_expires,
@ -63,7 +63,7 @@ async def refresh_access_token(
""" """
Return a new access token if the refresh token is valid Return a new access token if the refresh token is valid
""" """
access_token_expires = timedelta(minutes=30) access_token_expires = timedelta(minutes=1)
access_token = create_access_token( access_token = create_access_token(
data={"sub": current_user.id, "refresh": False}, data={"sub": current_user.id, "refresh": False},
expires_delta=access_token_expires, expires_delta=access_token_expires,

View File

@ -16,7 +16,7 @@ from app.util.authentication import get_current_user
router = APIRouter(prefix="/links", tags=["links"]) router = APIRouter(prefix="/links", tags=["links"])
@router.get("/", summary="Get all of the links associated with your account") @router.get("", summary="Get all of the links associated with your account")
async def get_links( async def get_links(
current_user: Annotated[User, Depends(get_current_user)], current_user: Annotated[User, Depends(get_current_user)],
db=Depends(get_db), db=Depends(get_db),
@ -35,7 +35,7 @@ async def get_links(
return links return links
@router.post("/", summary="Create a new link") @router.post("", summary="Create a new link")
async def create_link( async def create_link(
url: URLSchema, url: URLSchema,
current_user: Annotated[User, Depends(get_current_user)], current_user: Annotated[User, Depends(get_current_user)],

58
app/static/js/jwt.js Normal file
View File

@ -0,0 +1,58 @@
// Description: This file contains functions to access the API with JWT authentication.
/**
* Accept a full URL, method, and body to send to the API.
* - If successful, return the response
* - If first fail, attempt to refresh JWT token and try again
* - If second fail, return false
* @param {*} endpoint API endpoint
* @param {*} method String (GET, POST, PUT, DELETE)
* @param {*} body Data to send to the API
* @returns boolean
*/
async function accessAPI(endpoint, method, body) {
let response = await fetch(`/api${endpoint}`, {
method: method,
body: body,
});
if (response.ok) {
let data = await response.json();
data = await data;
console.log(data);
return data;
} else if (response.status === 401) {
console.log('REFRESHING TOKEN')
if (await refreshAccessToken()) {
// Try the request again
let response = await fetch(`/api${endpoint}`, {
method: method,
body: body,
});
if (response.ok) {
let data = await response.json();
data = await data;
console.log("REFRESHED DATA")
return data;
}
}
}
return false;
}
/**
* Attempt to refresh the JWT token
* @returns boolean
*/
async function refreshAccessToken () {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
});
if (response.ok) {
console.log("TOKEN REFRESH")
return true;
} else {
console.log("TOKEN REFRESH FAILED")
return false;
}
}

View File

@ -6,12 +6,13 @@
<title>LinkLogger | Dashboard</title> <title>LinkLogger | Dashboard</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<script src="/static/js/jwt.js"></script>
</head> </head>
<body> <body>
<div> <div>
<!-- Create a table with 4 columns with a total of 1000px width --> <!-- Create a table with 4 columns with a total of 1000px width -->
<table> <table>
<tr> <tr style="border: 2px solid #ccc;">
<th>Link</th> <th>Link</th>
<th>Visits</th> <th>Visits</th>
<th>Redirect</th> <th>Redirect</th>
@ -36,20 +37,30 @@
margin-top: 100px; margin-top: 100px;
} }
table { table {
margin: 20px 0 20px 0; margin: 20px 0 20px 0;
text-align: center; text-align: center;
font-size: 25px; font-size: 25px;
width: 1250px; width: 1000px;
color: #ccc; color: #ccc;
border-collapse: collapse; border-collapse: collapse;
overflow: hidden; overflow: hidden;
} }
/* Center all sub tables */
.log-table-row table {
margin: 0 auto;
}
.log-table-row table {
width: 90%;
}
table th { table th {
background-color: #415eac; background-color: #415eac;
padding: 10px;
border: 2px solid #ccc; border: 2px solid #ccc;
padding: 10px;
} }
.link-table-row { .link-table-row {
@ -91,25 +102,6 @@
</style> </style>
<script> <script>
async function fetchLinks() {
const response = await fetch('/api/links');
if (!response.ok) {
throw new Error('Failed to fetch links');
}
const links = await response.json();
return links;
}
async function fetchLogs(link) {
const response = await fetch(`/api/links/${link}/logs`);
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
const logs = await response.json();
return logs;
}
function createRow(index, link, logs) { function createRow(index, link, logs) {
// Create the sub-table with the logs // Create the sub-table with the logs
let subTable = ` let subTable = `
@ -124,10 +116,20 @@
`; `;
// Loop through the logs and create a row for each one // Loop through the logs and create a row for each one
logs.forEach((log, index) => { logs.forEach((log, index) => {
let logTimestamp = new Date(log.timestamp).toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(',', '');
let row = ` let row = `
<tr id="${log.id}-log"> <tr id="${log.id}-log">
<td>${logs.length - index}</td> <td>${logs.length - index}</td>
<td>${log.timestamp}</td> <td>${logTimestamp}</td>
<td>${log.ip}</td> <td>${log.ip}</td>
<td>${log.location}</td> <td>${log.location}</td>
<td>${log.isp}</td> <td>${log.isp}</td>
@ -172,21 +174,23 @@
async function getData() { async function getData() {
let links = fetchLinks(); const links = await accessAPI(`/links`, 'GET')
links = await links; if (!links) {
throw new Error('Failed to fetch links');
console.log(links); }
// Links is an Array of objects with the link data // Links is an Array of objects with the link data
// Loop through the links and create a row for each one // Loop through the links and create a row for each one
links.forEach(async (link, index) => { // Do not use async because then the order or data in the
// Fetch the logs for the link // table will change from time to time
let logs = await fetchLogs(link.link); for (let i = 0; i < links.length; i++) {
logs = await logs; let link = links[i];
// Create the entire row with sub-table and logs let logs = await accessAPI(`/links/${link.link}/logs`, 'GET')
let row = createRow(index, link, logs); if (!logs) {
// Add the new table row to the main table throw new Error('Failed to fetch logs');
}
let row = createRow(i, link, logs);
document.querySelector('table').innerHTML += row; document.querySelector('table').innerHTML += row;
}); }
} }
// hideLogRows to all log-table-rows // hideLogRows to all log-table-rows
@ -203,6 +207,7 @@
let id = event.target.id; let id = event.target.id;
let logTR = document.getElementById(`${id}-logTR`); let logTR = document.getElementById(`${id}-logTR`);
if (logTR.style.display === 'none') { if (logTR.style.display === 'none') {
// Hide any open log tables
hideLogRows(); hideLogRows();
logTR.style.display = 'table-row'; logTR.style.display = 'table-row';
} else { } else {
@ -214,6 +219,9 @@
// Add an event listen to all trash bins // Add an event listen to all trash bins
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
if (event.target.classList.contains('fa-trash')) { if (event.target.classList.contains('fa-trash')) {
// Confirm the user wants to delete the log
let confirmDelete = confirm('Are you sure you want to delete this log?');
if (confirmDelete) {
let id = event.target.id; let id = event.target.id;
let link = id.split('/')[1]; let link = id.split('/')[1];
let logId = id.split('/')[0]; let logId = id.split('/')[0];
@ -223,6 +231,7 @@
let logRow = document.getElementById(`${logId}-log`) let logRow = document.getElementById(`${logId}-log`)
logRow.remove(); logRow.remove();
} }
}
}); });
getData(); getData();

View File

@ -72,6 +72,10 @@ async def refresh_get_current_user(
return await get_current_user(token, is_refresh=True, db=db) return await get_current_user(token, is_refresh=True, db=db)
def process_refresh_token(token: str, db: Session):
return False
async def get_current_user( async def get_current_user(
request: Request, request: Request,
db=Depends(get_db), db=Depends(get_db),
@ -84,15 +88,6 @@ async def get_current_user(
Otherwise, the request is from an API and we should return a 401 Otherwise, the request is from an API and we should return a 401
""" """
# If the request is from /api/auth/refresh, it is a request to get
# a new access token using a refresh token
if request.url.path == "/api/auth/refresh":
token = request.cookies.get("refresh_token")
is_refresh = True
else:
token = request.cookies.get("access_token")
is_refresh = False
def raise_unauthorized(): def raise_unauthorized():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -100,6 +95,16 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
# If the request is from /api/auth/refresh, it is a request to get
# a new access token using a refresh token
if request.url.path == "/api/auth/refresh":
token = request.cookies.get("refresh_token")
user = process_refresh_token(token, db)
if user is None:
raise_unauthorized()
else:
token = request.cookies.get("access_token")
try: try:
payload = jwt.decode(token, secret_key, algorithms=[algorithm]) payload = jwt.decode(token, secret_key, algorithms=[algorithm])
id: int = payload.get("sub") id: int = payload.get("sub")