Skip to content

REST API endpoints

Dương Tiến Vinh edited this page Jul 2, 2022 · 9 revisions

Introduction:

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).

1. REST API endpoints:

Method URL Description
POST http://localhost:5000/api/v1/auth/login

Login user

POST http://localhost:5000/api/v1/auth/register

Register user

POST http://localhost:5000/api/v1/auth/logout

Logout user

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 image

GET http://localhost:5000/api/v1/users/{string:userId}/images/download-all

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 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 a 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)

2. Login:

⚠️ 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

3. Logout:

⚠️ 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

4. Register:

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

5. List images:

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

6. Upload image:

⚠️ 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

7. Download image:

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

8. Download ALL images:

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

9. Delete image:

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

10. Get user information:

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

11. Get all user information:

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

12. Get specific image permissions information:

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

13. Get image all permissions:

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

14. Share image with a specific user:

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

15. Edit one image permission:

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

16. Delete one image permission:

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

17. Download shared image:

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

18. Form validation error:

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"
}

19. Not Authorized error:

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"
}

20. Revoked token error:

The user tries to request with the revoked token.

Method Status Code Response
GET/POST/PUT/DELETE Error 401
{
  "message": "Token has been revoked"
}

21. Invalid token error:

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>'"
}