-
Notifications
You must be signed in to change notification settings - Fork 1
REST API endpoints
At first, the server I built was based entirely on session cookie-based authentication using the "Flask-Login" library. But after a few kinds of research, I switched to token-based authentication with "Flask-JWT-Extended" library, which uses JWT (JSON Web Token) to authenticate. So you may find some pieces of code that was use cookie I left behind.
You can use the file "api.py" to test API endpoints. For the sake of simplicity, I stored "JWT access token", "User id" as global variables for easy access. (You can also see that I stored cookie as a global variable too).
Method | URL | Description |
POST | http://localhost:5000/api/v1/auth/login | |
POST | http://localhost:5000/api/v1/auth/register | |
POST | http://localhost:5000/api/v1/auth/logout | |
GET | http://localhost:5000/api/v1/users | |
GET | http://localhost:5000/api/v1/users/{string:userId} | |
GET | http://localhost:5000/api/v1/users/{string:userId}/images | |
POST | http://localhost:5000/api/v1/users/{string:userId}/images | |
GET | http://localhost:5000/api/v1/users/{string:userId}/images/download-all | |
GET | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName} | |
DELETE | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName} | |
GET | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions | |
POST | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions | |
GET | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | |
PUT | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | |
DELETE | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | |
GET | http://localhost:5000/api/v1/users/{string:sharedUserId}/images/{string:fileName} |
⚠️ NOTE: Whenever users log in or log out, that means the user's session is over, so the cookie will be reset. Also, the JWT token will be sent to the blacklist.
Currently, when we log in, the JWT token is stored on the client persistently -> Vulnerable to CSRF & XSS attacks.
⚠️ NOTE: Each form has its cookie, so when we send a GET request to request a form to submit, we have to set a cookie for POST request
URL | http://localhost:5000/api/v1/auth/login | ||
Method | Status | Code | Response |
POST | Success | 200 |
{
"user_id": "628385eb1dc6fa1a0cd84c38",
"access_token": "eyJ0eXAiOiJ..."
} |
Example request
def login(username, password) -> Tuple[str, int]:
global access_token
global userId
login_p = requests.post(
"http://localhost:5000/api/v1/auth/login",
data={"username": username, "password": password},
)
data = json.loads(login_p.text)
if set(["user_id", "access_token"]).issubset(data.keys()):
access_token = data["access_token"]
userId = data["user_id"]
return login_p.text, login_p.status_code
⚠️ NOTE: I have turned off CSRF protection for the logout route, so we don't have to request a CSRF key.
- After user logged out, user's JWT token will be sent to block list (revoked), so attacker can't use the same token to log in.
- Currently not support retrieving revoked tokens.
URL | http://localhost:5000/api/v1/auth/logout | ||
Method | Status | Code | Response |
POST | Success | 200 |
{
"message": "User logged out"
} |
Example request
def logout() -> Tuple[str, int]:
global access_token
logout_p = requests.post(
"http://localhost:5000/api/v1/auth/logout",
headers={"Authorization": f"Bearer {access_token}"},
)
return logout_p.text, logout_p.status_code
After registering, the user is logged in, so the cookie is reset. User no
longer log in after registration.
When logged in, public and private for RSA algorithm is created for user at the current directory (the directory where the client is running):
-
Public key is saved with the file name: "rsa_pub.txt".
-
Private key is saved with the file name: "rsa.txt". If the file name already exists, then the file name will be appended with the timestamp. E.g: rsa_20220112162809.txt
URL | http://localhost:5000/api/v1/auth/register | ||
Method | Status | Code | Response |
POST | Success | 201 | Created - No response |
POST | Error | 409 |
{
"message": "Username already exists"
} |
Example request
def register(username, password) -> Tuple[str, int]:
e, d, n = Crypto.generateAndWriteKeyToFile("", writeFile=True)
register_p = requests.post(
"http://localhost:5000/api/v1/auth/register",
data={"username": username, "password": password, "publicKey": f"{n} {e}"},
)
return register_p.text, register_p.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images | ||
Method | Status | Code | Response |
GET | Success | 200 |
[
{
"img_name": "bicycle.png"
}
] |
GET | Success | 200 |
[] |
Example request
def listImage() -> Tuple[str, int]:
global access_token
global userId
list_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images",
headers={"Authorization": f"Bearer {access_token}"},
)
return list_img_g.text, list_img_g.status_code
⚠️ NOTE: Temporarily accepting .PNG image extension only.
When the user uploads an image (.png), the image is encrypted with a public key and returns the encrypted image along with the "quotient.txt". The quotient later is sent along with the image content.
- Why there is a quotient file?
When encrypting the image with RSA algorithm, the image is broken and can't open. The main purpose of quotient is used for modulo the encrypted message, so the image still can be opened, but the opener may or may not understand the image. This feature is intentionally implemented.
- Why server only accept .png files?
Well, the client can't decrypt other file extensions than .png after encrypted, so it's a one-way upload if you use other extensions. However, if you want, you can tweak accept file extensions in file app.py
- Where the images are saved?
In MongoDB cluster and local (./src/assets/). You can also change local save location in file app.py
URL | http://localhost:5000/api/v1/users/{string:userId}/images | ||
Method | Status | Code | Response |
POST | Success | 201 | Created - No response |
Example request
def uploadImage(fileName) -> Tuple[str, int]:
global access_token
global userId
global publicKey
if publicKey == "":
getUserInformation()
n, e = map(int, publicKey.split(" "))
# NOTE: "imageFile" is field from ImageForm class
name, ext = path.splitext(fileName)
fileName_encrypt = name + ext
Crypto.encrypt(
fileName,
n=n,
e=e,
imgEncryptedSaveDst=fileName_encrypt,
quotientSaveDst="quotient.txt",
)
q = open("quotient.txt", "r")
quotient = q.read()
q.close()
with open(fileName_encrypt, "rb") as f:
upload_img_p = requests.post(
f"http://localhost:5000/api/v1/users/{userId}/images",
# Please send a file with this format!
files={"imageFile": f},
data={"quotient": quotient},
headers={
"Authorization": f"Bearer {access_token}",
},
)
return upload_img_p.text, upload_img_p.status_code
The URI should not have the file extension.
The file is downloaded then the client uses the private key from local and the quotient content downloaded to decrypt the message.
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName} | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"img_content": "PNG\r\n\u001a\n\u0000...",
"img_name": "bicycle.png",
"quotient": "98 98 98 98 77 2 91 91..."
} |
GET | Error | 404 |
{
"message": "Image not found"
} |
Example request
def downloadImage(downloadFile, privateKeyPath) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(downloadFile)
downloadFile_d = name + ext
download_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}",
headers={
"Authorization": f"Bearer {access_token}",
},
)
if download_img_g.status_code != 200:
return download_img_g.text, download_img_g.status_code
data = json.loads(download_img_g.text)
imgData = data["img_content"]
imgName = data["img_name"]
quotientData = data["quotient"]
with open("quotient.txt", "w") as q:
q.write(quotientData)
with open(imgName, "wb") as f:
f.write(imgData.encode("ISO-8859-1"))
Crypto.decrypt(
imgEncryptedPath=downloadFile,
privateKeyPath=privateKeyPath,
imgDecryptedSaveDst=downloadFile_d,
)
return "", download_img_g.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images/download-all | ||
Method | Status | Code | Response |
GET | Success | 200 |
[
{
"img_content": "PNG\r\n\u001a\n\u0000...",
"img_name": "bicycle.png",
"quotient": "98 98 98 98 77 2 91 91..."
}
] |
GET | Success | 200 |
[] |
Example request
def downloadImageAll(pathPrivateKey) -> Tuple[str, int]:
global access_token
global userId
download_img_all_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/download-all",
headers={
"Authorization": f"Bearer {access_token}",
},
)
if download_img_all_g.status_code != 200:
return download_img_all_g.text, download_img_all_g.status_code
data = json.loads(download_img_all_g.text)
imgData = data
for image in imgData:
imgName = image["img_name"]
imgContent = image["img_content"]
quotientData = image["quotient"]
with open("quotient.txt", "w") as q:
q.write(quotientData)
with open(imgName, "wb") as f:
f.write(imgContent.encode("ISO-8859-1"))
Crypto.decrypt(
imgEncryptedPath=imgName,
privateKeyPath=pathPrivateKey,
imgDecryptedSaveDst=imgName,
)
return "", download_img_all_g.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName} | ||
Method | Status | Code | Response |
DELETE | Success | 204 | No Content - No response |
DELETE | Error | 404 |
{
"message": "Image not found"
} |
Example request
def deleteImage(deleteFile) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(deleteFile)
delete_img_d = requests.delete(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}",
headers={
"Authorization": f"Bearer {access_token}",
},
)
return delete_img_d.text, delete_img_d.status_code
URL | http://localhost:5000/api/v1/users/{string:userId} | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"public_key": "27977 9431",
"user_id": "628385eb1dc6fa1a0cd84c38",
"user_name": "admin"
} |
GET | Error | 404 |
{
"message": "User not found"
} |
Example request
def getUserInformation() -> Tuple[str, int]:
global access_token
global userId, userName, publicKey
user_info_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}",
headers={
"Authorization": f"Bearer {access_token}",
},
)
if user_info_g.status_code == 200:
user_info_g_data = json.loads(user_info_g.text)
userId = user_info_g_data["user_id"]
userName = user_info_g_data["user_name"]
publicKey = user_info_g_data["public_key"]
return user_info_g.text, user_info_g.status_code
URL | http://localhost:5000/api/v1/users | ||
Method | Status | Code | Response |
GET | Success | 200 |
[
{
"public_key": "27977 9431",
"user_id": "628385eb1dc6fa1a0cd84c38",
"user_name": "admin"
}
] |
GET | Success | 200 |
[] |
Example request
def getAllUserInformation() -> Tuple[str, int]:
global access_token
user_info_g = requests.get(
"http://localhost:5000/api/v1/users",
headers={
"Authorization": f"Bearer {access_token}",
},
)
return user_info_g.text, user_info_g.status_code
Only return one permission that matches the sharedUserId.
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"userId": "628387db1dc6fa1a0cd84c42",
"role": "write"
} |
GET | Error | 404 |
{
"message": "Permission for User id not found"
} |
GET | Error | 404 |
{
"message": "Image not found"
} |
Example request
def getShareImageInfo(fileShare, sharedUserId) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(fileShare)
permission_info_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}", # noqa
headers={
"Authorization": f"Bearer {access_token}",
},
)
return permission_info_g.text, permission_info_g.status_code
Return a list of permissions for the image. This response also includes a CSRF token for POST request later.
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions | ||
Method | Status | Code | Response |
GET | Success | 200 |
[
{
"userId": "628387db1dc6fa1a0cd84c42",
"role": "write"
}
] |
GET | Success | 200 |
[] |
GET | Error | 404 |
{
"message": "Image not found"
} |
Example request
def getShareImageAllInfo(fileShare) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(fileShare)
permission_info_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions",
headers={
"Authorization": f"Bearer {access_token}",
},
)
return permission_info_g.text, permission_info_g.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions | ||
Method | Status | Code | Response |
POST | Success | 201 | Created - No response |
POST | Error | 409 |
{
"message": "Permission user id is already exists"
} |
GET | Error | 404 |
{
"message": "Image not found"
} |
Example request
def shareImage(fileShare, userPermission, role) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(fileShare)
permission_info_p = requests.post(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions",
data={"user_id": userPermission, "role": role},
headers={
"Authorization": f"Bearer {access_token}",
},
)
return permission_info_p.text, permission_info_p.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | ||
Method | Status | Code | Response |
PUT | Success | 204 | No Content - No response |
PUT | Error | 404 |
{
"message": "Permission for User id not found"
} |
PUT | Error | 404 |
{
"message": "Image not found"
} |
Example request
def editImagePermissions(fileShare, sharedUserId, role) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(fileShare)
permission_info_p = requests.put(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}", # noqa
data={"role": role},
headers={
"Authorization": f"Bearer {access_token}",
},
)
return permission_info_p.text, permission_info_p.status_code
URL | http://localhost:5000/api/v1/users/{string:userId}/images/{string:fileName}/permissions/{string:userPermissionId} | ||
Method | Status | Code | Response |
DELETE | Success | 204 | No Content - No response |
DELETE | Error | 404 |
{
"message": "Permission for User id not found"
} |
DELETE | Error | 404 |
{
"message": "Image not found"
} |
Example request
def deleteImagePermissions(fileShare, sharedUserId) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(fileShare)
permission_info_d = requests.delete(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}", # noqa
headers={
"Authorization": f"Bearer {access_token}",
},
)
return permission_info_d.text, permission_info_d.status_code
Since the database didn't store a private key, so the client can't decrypt the image for user
URL | http://localhost:5000/api/v1/users/{string:sharedUserId}/images/{string:fileName} | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"img_name": "bicycle.png",
"img_content": "\u00ff...",
"quotient": "22 22..."
} |
GET | Error | 404 |
{
"message": "Image not found"
} |
Example request
def getShareImage(downloadFile, sharedUserId) -> Tuple[str, int]:
global access_token
global userId
name, ext = path.splitext(downloadFile)
download_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{sharedUserId}/images/{name}",
headers={
"Authorization": f"Bearer {access_token}",
},
)
if download_img_g.status_code != 200:
return download_img_g.text, download_img_g.status_code
data = json.loads(download_img_g.text)
imgData = data["img_content"]
imgName = data["img_name"]
quotientData = data["quotient"]
with open("quotient.txt", "w") as q:
q.write(quotientData)
with open(imgName, "wb") as f:
f.write(imgData.encode("ISO-8859-1"))
# Since the db didn't store the private, so the file can only be downloaded
return "", download_img_g.status_code
Before each POST request, typically the client has to send a GET request to get the html form with the CSRF token. But with this server, the client only gets CSRF token, then the user sends a POST request with the form content within the request body. The request body then passed in the form class and was validated by the form. If the form content is failed, then this response is sent back to the client.
Method | Status | Code | Response |
POST | Error | 422 |
{
"message": "Username or password is invalid"
} |
POST | Error | 422 |
{
"message": "Password is required"
} |
The user ID of decoded JWT token doesn't match the resources we request.
Method | Status | Code | Response |
GET/POST/PUT/DELETE | Error | 401 |
{
"message": "User is not authorized"
} |
The user tries to request with the revoked token.
Method | Status | Code | Response |
GET/POST/PUT/DELETE | Error | 401 |
{
"message": "Token has been revoked"
} |
The user tries to request with the missing token or invalid token. The message may vary.
Method | Status | Code | Response |
GET/POST/PUT/DELETE | Error | 422 |
{
"message": "Bad Authorization header. Expected 'Authorization: Bearer <JWT>'"
} |