python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
flask run
At first, the server I built was based entirely on session cookie based authentication using "Flask-Login" library. But after a few researches, I switched to token based authentication with "Flask-JWT-Extended" library, which use JWT (JSON Web Token) to authenticate. So you may find some pieces of code that was use cookie I left behind.
You can use file "old_client.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 also stored cookie as global variable too).
Method | URL | Description |
GET | http://localhost:5000/api/v1/users | Get all users information |
GET | http://localhost:5000/api/v1/users/<string:userId> | Get user information |
GET | http://localhost:5000/api/v1/users/<string:userId>/images | Get user all images |
POST | http://localhost:5000/api/v1/users/<string:userId>/images/upload | Upload image |
GET | http://localhost:5000/api/v1/users/<string:userId>/images/data | Download all images |
GET | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName> | Download specific image |
DELETE | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/delete | Delete specific image |
GET | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions | Get all image permissions |
POST | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions | Share image to specific user (Grant permission) |
GET | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions/<string:userPermissionId> | Get specific permission of image |
PUT | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions/<string:userPermissionId> | Edit specific permission of image |
DELETE | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions/<string:userPermissionId> | Delete specific permission of image |
GET | http://localhost:5000/api/v1/users/<string:sharedUserId>/images/<string:fileName> | Download shared image (the same as download specific image) |
⚠️ NOTE: Whenever user login or logout, that means user's session is over, so cookie will be reset. Also, the JWT token will be sent to blacklist.
Currently when we login, the JWT token is stored on the client persistently -> Vulnerable to CSRF & XSS attacks.
⚠️ NOTE: Each form has its own cookie, so when we send a GET request to request a form to submit, we have to set cookie for POST request
URL | http://localhost:5000/login | ||
Method | Status | Code | Response |
GET | Success | 200 |
{ "csrf_token": "eyJ0eXAi..." } |
POST | Success | 200 |
{ "access_token": "eyJ0eXAi...", "user_id": "61dea576762674330a3f17dc" } |
Code implementation
def login(username, password):
# global cookie
global access_token
global userId
login_g = requests.get("http://localhost:5000/login")
login_data = json.loads(login_g.text)
csrfKey = login_data["csrf_token"]
cookie = login_g.headers["Set-Cookie"]
# print("login_p", login_g.text)
# NOTE: When login, cookie will be reset
login_p = requests.post(
"http://localhost:5000/login",
data={"username": username, "password": password},
headers={
"X-CSRFToken": csrfKey,
"Cookie": cookie,
},
)
print("login_p", login_p.text)
data = json.loads(login_p.text)
if data:
access_token = data["access_token"]
userId = data["user_id"]
# print("access_token", access_token)
return str('{"data": {"user id": "%s"}}' % str(data["user_id"]))
# cookie = login_p.headers["Set-cookie"]
⚠️ NOTE: I have turned off CSRF protection for logout route, so we don't have to request a CSRF key.
URL | http://localhost:5000/logout | ||
Method | Status | Code | Response |
POST | Success | 200 |
{ "status": "success", "code": "200", "data": "User logged out" } |
Code implementation
def logout():
# global cookie
global access_token
logout_p = requests.post(
"http://localhost:5000/logout",
# headers={"Cookie": cookie},
headers={"Authorization": f"Bearer {access_token}"},
)
# print("logout", logout_p.text)
return logout_p.text
# cookie = logout_p.headers["Set-Cookie"]
After register, user is logged in, so cookie is reset. User no longer login
after registration.
When logged in, public and private for RSA algorithm is created for user at current directory (directory where client is running):
-
Public key is save with file name: "rsa_pub.txt".
-
Private key is save with file name: "rsa.txt". If the file name is already exists, then the file name will be append with the timestamp. E.g: rsa_20220112162809.txt
URL | http://localhost:5000/register | ||
Method | Status | Code | Response |
GET | Success | 200 |
{ "csrf_token": "eyJ0eXAi..." } |
POST | Success | 201 | Created - No response |
POST | Error | 409 |
{ "status": "error", "code": "409", "message": "Username already exists" } |
Code implementation
def register(username, password):
# global cookie
register_g = requests.get("http://localhost:5000/register")
register_data = json.loads(register_g.text)
csrfKey = register_data["csrf_token"]
cookie = register_g.headers["Set-Cookie"]
# print("register_g", register_g.text)
e, d, n = function_support.create_write_key("", writeFile=True)
register_p = requests.post(
"http://localhost:5000/register",
data={"username": username, "password": password, "publicKey": f"{n} {e}"},
headers={
"X-CSRFToken": csrfKey,
"Cookie": cookie,
},
)
# print("register_p", register_p.text)
return register_p.text
URL | http://localhost:5000/api/v1/users/<string:userId>/images | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": ["traffic-sign.png", "bicycle.png"]
} |
GET | Success | 200 |
{ "status": "success", "code": "200", "data": [] } |
Code implementation
# global cookie
global access_token
global userId
list_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images",
# headers={"Cookie": cookie},
headers={"Authorization": f"Bearer {access_token}"},
)
# print("list_img_g", list_img_g.text)
return list_img_g.text
⚠️ NOTE: Temporarily accepting .PNG image extension only.
When user upload a image (.png), the image is encrypted with public key and return 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 encrypt the image with RSA algorithm, the image is broken and can't open. Use quotient is use for modulo the encrypt message, so the image still can be opened, but the opener may or may not understand the image.
URL | http://localhost:5000/api/v1/users/<string:userId>/images/upload | ||
Method | Status | Code | Response |
GET | Success | 200 |
{ "csrf_token": "eyJ0eXAi..." } |
POST | Success | 200 |
{
"status": "success",
"code": "200",
"data": { "img_name": "bicycle.png_20220109213826" }
} |
Code implementation
# global cookie
global access_token
global userId
global publicKey
# public_key_g = requests.get(
# "http://localhost:5000/api/v1/users/<string:userId>/public-key",
# headers={"Cookie": cookie},
# )
# public_key_data = json.loads(public_key_g.text)
# print("public_key_data", public_key_data)
if publicKey == "":
getUserInformation()
n, e = map(int, publicKey.split(" "))
upload_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/upload",
# headers={"Cookie": cookie},
headers={"Authorization": f"Bearer {access_token}"},
)
upload_img_data = json.loads(upload_img_g.text)
csrfKey = upload_img_data["csrf_token"]
cookie = upload_img_g.headers["Set-Cookie"]
# print("upload_img_g", upload_img_g.text)
# **⚠️ NOTE:** "imageFile" is field from ImageForm class
# fileName = "bicycle2.png"
name, ext = path.splitext(fileName)
fileName_encrypt = name + "_e" + ext
function_support.Encrypted(
fileName,
n=n,
e=e,
save_imageEncrypted=fileName_encrypt,
save_quotient="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/upload",
files={"imageFile": f},
data={"quotient": quotient},
headers={
"X-CSRFToken": csrfKey,
"Cookie": cookie,
"Authorization": f"Bearer {access_token}",
},
)
# print("upload_img_p", upload_img_p.text)
return upload_img_p.text
The URI should not have the file extension.
The file is downloaded then client use 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 |
{
"status": "success",
"code": "200",
"data": {
"img_name": "bicycle.png",
"img_content": "\u00ff...",
"quotient": "22 22..."
}
} |
GET | Error | 404 |
{ "status": "error", "code": "404", "message": "Image not found" } |
Code implementation
# global cookie
global access_token
global userId
# downloadFile = "bicycle2_e.png"
name, ext = path.splitext(downloadFile)
downloadFile_d = name + "_d" + ext
download_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}",
# headers={"Cookie": cookie},
headers={
"Authorization": f"Bearer {access_token}",
},
)
data = json.loads(download_img_g.text)
imgData = data["data"]["img_content"]
imgName = data["data"]["img_name"]
quotientData = data["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"))
function_support.Decrypted(
path_ImageDecode=downloadFile,
path_private_key=privateKeyPath,
save_imageDecrypted=downloadFile_d,
)
URL | http://localhost:5000/api/v1/users/<string:userId>/images/data | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": [
{
"img_name": "bicycle.png",
"img_content": "\u00ff...",
"quotient": "22 22..."
}
]
} |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": []
} |
Code implementation
def downloadImageAll(pathPrivateKey):
# global cookie
global access_token
global userId
download_img_all_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/data",
# headers={"Cookie": cookie},
headers={
"Authorization": f"Bearer {access_token}",
},
)
data = json.loads(download_img_all_g.text)
imgData = data["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"))
function_support.Decrypted(
path_ImageDecode=imgName,
path_private_key=pathPrivateKey,
save_imageDecrypted=imgName,
)
URL | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/delete | ||
Method | Status | Code | Response |
GET | Success | 200 |
{ "csrf_token": "eyJ0eXAi..." } |
DELETE | Success | 204 | No Content - No response |
DELETE | Error | 404 |
{ "status": "error", "code": "404", "message": "Image not found" } |
Code implementation
def deleteImage(deleteFile):
# global cookie
global access_token
global userId
# deleteFile = "bicycle2_e.png"
name, ext = path.splitext(deleteFile)
delete_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/delete",
# headers={"Cookie": cookie}
headers={
"Authorization": f"Bearer {access_token}",
},
)
delete_img_g_data = json.loads(delete_img_g.text)
print("delete_img_g_data", delete_img_g_data)
csrfKey = delete_img_g_data["csrf_token"]
cookie = delete_img_g.headers["Set-Cookie"]
delete_img_d = requests.delete(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/delete",
headers={
"X-CSRFToken": csrfKey,
"Cookie": cookie,
"Authorization": f"Bearer {access_token}",
},
)
# delete_img_d_data = json.loads(delete_img_d.text)
# print("delete_img_p_data", delete_img_d_data)
URL | http://localhost:5000/api/v1/users/<string:userId> | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": {
"user_id": "a23415...",
"user_name": "admin",
"public_key": "118403 97093"
}
} |
GET | Error | 404 |
{ "status": "error", "code": "404", "message": "User not found" } |
Code implementation
def getUserInformation():
# global cookie
global access_token
global userId, userName, publicKey
user_info_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}",
headers={
# "Cookie": cookie,
"Authorization": f"Bearer {access_token}",
},
)
user_info_g_data = json.loads(user_info_g.text)
# print("public_key_g_data", user_info_g_data)
userId = user_info_g_data["data"]["user_id"]
userName = user_info_g_data["data"]["user_name"]
publicKey = user_info_g_data["data"]["public_key"]
return str(
'{"data": {"user id": "%s", "userName": "%s", "publicKey": "%s"}}'
% (str(userId), str(userName), str(publicKey))
)
URL | http://localhost:5000/api/v1/users | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": [
{
"user_id": "a23415...",
"user_name": "admin",
"public_key": "118403 97093"
}
]
} |
GET | Success | 200 |
{ "status": "success", "code": "200", "data": [] } |
Code implementation
def getAllUserInformation():
# global cookie
global access_token
user_info_g = requests.get(
f"http://localhost:5000/api/v1/users",
headers={
# "Cookie": cookie,
"Authorization": f"Bearer {access_token}",
},
)
user_info_g_data = json.loads(user_info_g.text)
print("public_key_g_data", user_info_g_data)
Only return one permissions which match the sharedUserId.
URL | http://localhost:5000/api/v1/users/<string:userId>/images/<string:fileName>/permissions/<string:userPermissionId> | ||
Method | Status | Code | Response |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": { "userId": "61de598f170caaeac86ce44d", "role": "write" }
} |
GET | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Permission for User id not found"
} |
GET | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def getShareImageInfo(fileShare, sharedUserId):
global access_token
global userId
# fileShare = "bicycle2_e.png"
# sharedUserId = "61dd6f75cb9aa4cea4a70f0c"
name, ext = path.splitext(fileShare)
permission_info_g = requests.get(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}",
headers={
"Authorization": f"Bearer {access_token}",
},
)
permission_info_g_data = json.loads(permission_info_g.text)
print("permission_info_g_data", permission_info_g_data)
Return a list of permissions for image. This response also include 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 |
{
"status": "success",
"code": "200",
"data": {
"permissions": [{ "userId": "61de598f170caaeac86ce44d", "role": "write" }],
"csrf_token": "eyJ0eXAi..."
}
} |
GET | Success | 200 |
{
"status": "success",
"code": "200",
"data": {
"permissions": [],
"csrf_token": "eyJ0eXAi..."
}
} |
GET | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def getShareImageAllInfo(fileShare):
global access_token
global userId
# fileShare = "bicycle2_e.png"
userPermission = "61dd6f75cb9aa4cea4a70f0c"
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}",
},
)
permission_info_g_data = json.loads(permission_info_g.text)
print("permission_info_g_data", permission_info_g_data)
cookie = permission_info_g.headers["Set-Cookie"]
csrfKey = permission_info_g_data["csrf_token"]
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 |
{
"status": "error",
"code": "409",
"message": "Permission user id is already exists"
} |
GET | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def shareImage(fileShare, userPermission, role):
global access_token
global userId
# fileShare = "bicycle2_e.png"
# userPermission = "61dd6f75cb9aa4cea4a70f0c"
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}",
},
)
permission_info_g_data = json.loads(permission_info_g.text)
# print("permission_info_g_data", permission_info_g_data)
cookie = permission_info_g.headers["Set-Cookie"]
csrfKey = permission_info_g_data["csrf_token"]
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}",
"Cookie": cookie,
"X-CSRFToken": csrfKey,
},
)
if permission_info_p.text:
permission_info_p_data = json.loads(permission_info_p.text)
return permission_info_p.text
# print("permission_info_g_data", permission_info_p_data)
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 |
{
"status": "error",
"code": "404",
"message": "Permission for User id not found"
} |
PUT | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def editImagePermissions(fileShare, sharedUserId, role):
global access_token
global userId
# fileShare = "bicycle2_e.png"
# sharedUserId = "61dd6f75cb9aa4cea4a70f0c"
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}",
},
)
permission_info_g_data = json.loads(permission_info_g.text)
print("permission_info_g_data", permission_info_g_data)
cookie = permission_info_g.headers["Set-Cookie"]
csrfKey = permission_info_g_data["csrf_token"]
permission_info_p = requests.put(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}",
data={"role": role},
headers={
"Authorization": f"Bearer {access_token}",
"Cookie": cookie,
"X-CSRFToken": csrfKey,
},
)
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 |
{
"status": "error",
"code": "404",
"message": "Permission for User id not found"
} |
DELETE | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def editImagePermissions(fileShare, sharedUserId, role):
global access_token
global userId
# fileShare = "bicycle2_e.png"
# sharedUserId = "61dd6f75cb9aa4cea4a70f0c"
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}",
},
)
permission_info_g_data = json.loads(permission_info_g.text)
print("permission_info_g_data", permission_info_g_data)
cookie = permission_info_g.headers["Set-Cookie"]
csrfKey = permission_info_g_data["csrf_token"]
permission_info_p = requests.put(
f"http://localhost:5000/api/v1/users/{userId}/images/{name}/permissions/{sharedUserId}",
data={"role": role},
headers={
"Authorization": f"Bearer {access_token}",
"Cookie": cookie,
"X-CSRFToken": csrfKey,
},
)
Since the database didn't store private key, so 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 |
{
"status": "success",
"code": "200",
"data": {
"img_name": "bicycle.png",
"img_content": "\u00ff...",
"quotient": "22 22..."
}
} |
GET | Error | 404 |
{
"status": "error",
"code": "404",
"message": "Image not found"
} |
Code implementation
def getShareImage(downloadFile, sharedUserId):
# global cookie
global access_token
global userId
# downloadFile = "bicycle2_e.png"
# sharedUserId = "61dd6f75cb9aa4cea4a70f0c"
name, ext = path.splitext(downloadFile)
downloadFile_d = "bicycle_d.png"
download_img_g = requests.get(
f"http://localhost:5000/api/v1/users/{sharedUserId}/images/{name}",
# headers={"Cookie": cookie},
headers={
"Authorization": f"Bearer {access_token}",
},
)
data = json.loads(download_img_g.text)
imgData = data["data"]["img_content"]
imgName = data["data"]["img_name"]
quotientData = data["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
# function_support.Decrypted(
# path_ImageDecode=downloadFile,
# path_private_key="rsa.txt",
# save_imageDecrypted=downloadFile_d,
# )
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, client only get CSRF token, then user send a POST request with the form content within the request body. The request body then passed in the form class and validated by the form. If the form content is failed, then the this response is sent back to client.
Method | Status | Code | Response |
POST | Error | 422 |
{
"status": "error",
"message": "Username or password is invalid"
} |
POST | Error | 422 |
{
"status": "error",
"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 |
{ "status": "error", "code": "401", "message": "User is not authorized" } |
User tries to request with the revoked token.
Method | Status | Code | Response |
GET/POST/PUT/DELETE | Error | 401 |
{ "status": "error", "code": "401", "message": "Token has been revoked" } |
User tries to request with missing token or invalid token. The message may vary.
Method | Status | Code | Response |
GET/POST/PUT/DELETE | Error | 422 |
{
"status": "error",
"code": "422",
"message": "Bad Authorization header. Expected 'Authorization: Bearer <JWT>'"
} |
- Set expiration time for token (NOTE: Added but don't know if it really works)
- Allow user to get back revoked token.
- Handle expired token error.
- Add validator for only .PNG image file.
- Support more image extensions, more file types.
- Don't create key if registration failed.