Issue with python code to decrypt the app context

Zoom Apps Configuration
The app frontend is React + NextJS, and the backend is running Python + Django, with auth purely on our side (no 3rd party). The app is to be embedded in a Zoom desktop.

We are trying the in-client auth feature with the goal to verify & identify the logged in user in Desktop & then issue him Application Auth token pair, so that the user can access Application functionality. Here are the steps:

  1. Verify in the frontend from the user is authorized or not. If not authorized, call ZoomSdk.authorize
  2. If authorized, get App Context & send it our backend API
  3. The backend API decrypts the token (the Zoom app client secret used to decode it also verifies it), identifies the user & issues the application token pair.
  4. The frontend continues to listen to:
  • userContext changes to see if it needs to authorize again
  • periodically refresh the application token pair using the refresh token
  1. The frontend gets the application data using the token pair above

I have 2 questions:

  1. Is the above flow correct? Specifically - also in this case in the frontend, is ZoomSdk.authorize() or .promptAuthorize() better to call?
  2. I’m getting an error when decrypting the Zoom App token in the backend. Logic & sample code is below. Need help debugging this.

Error?

    def decode_base64url(self, input_str):
        # Replace base64url characters with base64 equivalents
        input_str = input_str.replace('-', '+').replace('_', '/')
        # Pad with '=' to make the length a multiple of 4
        padding = len(input_str) % 4
        if padding > 0:
            input_str += '=' * (4 - padding)
        return base64.b64decode(input_str)


    def decrypt_zoom_app_context(self, context, secret_key=None):
        client_secret = settings.ZOOM_PLUGIN.get('SPEAKER').get('CLIENT_SECRET')
        context_bytes = self.decode_base64url(context)

        iv_length = context_bytes[0]

        iv = context_bytes[1:1 + iv_length]
        aad_length = int.from_bytes(context_bytes[1 + iv_length:3 + iv_length], byteorder='little')

        aad = context_bytes[3 + iv_length:3 + iv_length + aad_length]
        cipher_length = int.from_bytes(context_bytes[3 + iv_length + aad_length:7 + iv_length + aad_length], byteorder='little')

        cipher_text = context_bytes[7 + iv_length + aad_length:7 + iv_length + aad_length + cipher_length]
        tag = context_bytes[7 + iv_length + aad_length + cipher_length:]

                derive = HKDF(
            algorithm=hashes.SHA256(),
            length=32,
            salt=None,
            info=b'handshake data',
            backend=default_backend()
        )
        key = derive.derive(client_secret.encode())
        # Setup cipher
        cipher = Cipher(
            algorithms.AES(key),
            modes.GCM(iv, tag),
            backend=default_backend()
        )
        decryptor = cipher.decryptor()
        decrypted = decryptor.update(cipher_text) + decryptor.finalize()

        return json.loads(decrypted.decode('utf-8'))

I’m getting the below error in decryptor.finalize()

    decrypted = decryptor.update(cipher_text) + decryptor.finalize()
                                                ^^^^^^^^^^^^^^^^^^^^
  File "/Users/arvind/venv/lib/python3.11/site-packages/cryptography/hazmat/primitives/ciphers/base.py", line 229, in finalize
    data = self._ctx.finalize()
           ^^^^^^^^^^^^^^^^^^^^
  File "/Users/arvind/venv/lib/python3.11/site-packages/cryptography/hazmat/backends/openssl/ciphers.py", line 200, in finalize
    raise InvalidTag
cryptography.exceptions.InvalidTag

Troubleshooting Routes
The intermediate debugging values look reasonable - ie iv has length 12, AAD is 0 length, cipher is 216 chars & tag 16 chars. Base64 encoded input was 335 length, with the decoded one 251.

How To Reproduce

Call decrypt_zoom_app_context with this one value I got from zoom -

DGXwvj7p0Xbh7brIgAAA2AAAAEikLl9deBnPNlaABm9UNSyNOQttdQ9Lr9Nw9XDbhX09MtgaO_jAZkObkQDwjb13y-fB75FksOvhWJlKA_jiH5zq18Q4jrNUYigklz_syAKMDTu_k5L1eDRX_iMgjfAiIIh8r9uwzZp_oqIdiqEHYpm-8EF74B88Dnjx-lDmarqvz8dwi5RvajwqdQJVXDRqVNCOGz9JR90r1Fw75I9AKNDCOw7KeLja96ofIlA3f3tEbAU7ozDBQ63uosCaoqoU0lLi8sb8sRxRdQLaKHgZokyLewHLRNBeuEv_oQLQO6Qvtp8659xPo84

Yes, this is correct. You only need to use promptAuthorize if the user is not signed in and you are supporting Guest Mode.

More information on that function is here.

We have a code sample on using python to decrypt the context that you can find here:

Let me know if that helps.

Hi @MaxM great - this is helpful. I’m able to use this code to decode the App context. Quick question - how do I convert the Zoom uids eg - MPjq6UmhQWWBP4Yuf-FARg to get the user’s email? Our user ids are emails, and we need this email to look it up.

We don’t have the user’s JWT on the server (not keeping it so far) - so we can’t lookup the the user details via the below API

url = f"https://api.zoom.us/v2/users/{user_uid}"
headers = {
“Authorization”: f"Bearer {jwt_token}",
“Content-Type”: “application/json”
}

Any other way to translate the uid to emails on the server using the application client id / secrets?
Thanks
Arvind

We do not have a method to obtain emails (or any other PII) without the use of our API as that allows for a more granular authorization flow.

If there is a moment when you have the JWT on the server, you can make a request to obtain user information at that point. Otherwise, you would want to securely store the token for use later.