Search code examples
python-3.xboxpythonanywhere

Boxsdk JWTAuth keeps giving me this error: 'NoneType' object has no attribute 'encode'


I am trying to write a small script in python to connect to BOX, but it keeps giving me this error: 'NoneType' object has no attribute 'encode'

At first I thought it was caused when I encoded the Passphrase, but it seems that's not the case. The script fails when I try to authenticate with access_token = auth.authenticate_instance(). If I run the script without that it seems to work. What could be causing this?

Thanks in advance for any help you can provide.

This is what I have:

import keyring
from boxsdk import JWTAuth
from boxsdk import Client

def read_tokens():
    """Reads authorisation tokens from keyring"""
    # Use keyring to read the tokens
    auth_token = keyring.get_password('Box_Auth', 'mybox@box.com')
    refresh_token = keyring.get_password('Box_Refresh', 'mybox@box.com')
    return auth_token, refresh_token


def store_tokens(access_token, refresh_token):
    """Callback function when Box SDK refreshes tokens"""
    # Use keyring to store the tokens
    keyring.set_password('Box_Auth', 'mybox@box.com', access_token)
    keyring.set_password('Box_Refresh', 'mybox@box.com', refresh_token)

Passphrase = 'xxxxxxx';
my_str_as_bytes = Passphrase.encode('UTF-8','strict')

auth = JWTAuth(
    client_id='xxxxxxxxxx',
    client_secret='xxxxxxxx',
    enterprise_id='xxxxxxx',
    jwt_key_id='xxxxxxx',
    rsa_private_key_file_sys_path='/home/Marketscale/keys/private_key2.pem',
    rsa_private_key_passphrase=my_str_as_bytes,
    store_tokens=store_tokens,
)

access_token = auth.authenticate_instance()

This is the full text of the error:

Traceback (most recent call last):
  File "/home/Marketscale/Tests/JWTTest.py", line 37, in <module>
    access_token = auth.authenticate_instance()
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/jwt_auth.py", line 186, in authenticate_instance
    return self._auth_with_jwt(self._enterprise_id, 'enterprise')
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/jwt_auth.py", line 158, in _auth_with_jwt
    return self.send_token_request(data, access_token=None, expect_refresh_token=False)[0]
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/oauth2.py", line 298, in send_token_request
    self._store_tokens(access_token, refresh_token)
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/oauth2.py", line 233, in _store_tokens
    self._store_tokens_callback(access_token, refresh_token)
  File "/home/Marketscale/Tests/JWTTest.py", line 22, in store_tokens
    keyring.set_password('Box_Refresh', 'mybox@box.com', refresh_token)
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/keyring/core.py", line 48, in set_password
    _keyring_backend.set_password(service_name, username, password)
  File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/keyrings/alt/file_base.py", line 128, in set_password
    password_encrypted = self.encrypt(password.encode('utf-8'), assoc)
AttributeError: 'NoneType' object has no attribute 'encode'

Solution

  • I don't know the API you're using at all, but a few thoughts based on looking at the code:

    Working through the stack trace from the bottom up, you have:

     File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/keyrings/alt/file_base.py", line 128, in set_password
        password_encrypted = self.encrypt(password.encode('utf-8'), assoc)
    AttributeError: 'NoneType' object has no attribute 'encode'
    

    That code is at https://github.com/jaraco/keyrings.alt/blob/master/keyrings/alt/file_base.py, and the password (which we know is None) is the last parameter passed in to the set_password function. That is called from:

      File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/keyring/core.py", line 48, in set_password
        _keyring_backend.set_password(service_name, username, password)
    

    That code is at https://github.com/jaraco/keyring/blob/master/keyring/core.py, and the password is again the last parameter to the set_password function. Next, we have:

      File "/home/Marketscale/Tests/JWTTest.py", line 22, in store_tokens
        keyring.set_password('Box_Refresh', 'mybox@box.com', refresh_token)
    

    ....which is your code, so refresh_token must have been None. This means that your store_tokens must have been called with a refresh_token of None. Next:

      File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/oauth2.py", line 233, in _store_tokens
        self._store_tokens_callback(access_token, refresh_token)
    

    This is at https://github.com/box/box-python-sdk/blob/master/boxsdk/auth/oauth2.py, and once again means that _store_tokens was called with refresh_token set to None. Onwards...

      File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/oauth2.py", line 298, in send_token_request
        self._store_tokens(access_token, refresh_token)
    

    Code on the same page as the last, but now it's a bit more interesting:

            url = '{base_auth_url}/token'.format(base_auth_url=API.OAUTH2_API_URL)
            headers = {'content-type': 'application/x-www-form-urlencoded'}
            network_response = self._network_layer.request(
                'POST',
                url,
                data=data,
                headers=headers,
                access_token=access_token,
            )
            if not network_response.ok:
                raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
            try:
                response = network_response.json()
                access_token = response['access_token']
                refresh_token = response.get('refresh_token', None)
                if refresh_token is None and expect_refresh_token:
                    raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
            except (ValueError, KeyError):
                raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
            self._store_tokens(access_token, refresh_token)
            return self._access_token, self._refresh_token
    

    So we know that self._store_tokens was called with refresh_token set to None, which means that expect_refresh_token must have been False, as otherwise the BoxOAuthException would have been raised. And, indeed, if we look at the next line up in the stack trace we can see that:

      File "/home/Marketscale/.virtualenvs/myvirtualenv/lib/python3.5/site-packages/boxsdk/auth/jwt_auth.py", line 158, in _auth_with_jwt
        return self.send_token_request(data, access_token=None, expect_refresh_token=False)[0]
    

    That suggests to me that when you're using JWT Auth, you should not expect a refresh token. And given that the file backend for the keyring explodes when you pass it a None as a password, it sounds like you need to handle the None case differently. So, I'd suggest changing the store_tokens function you provide so that either it ignores the refresh token if it's None, ie:

    def store_tokens(access_token, refresh_token):
        """Callback function when Box SDK refreshes tokens"""
        # Use keyring to store the tokens
        keyring.set_password('Box_Auth', 'mybox@box.com', access_token)
        if refresh_token is not None:
            keyring.set_password('Box_Refresh', 'mybox@box.com', refresh_token)
    

    ...or so that it converts None into something that the keyring file backend can gracefully handle -- maybe an empty string would do the trick:

        def store_tokens(access_token, refresh_token):
            """Callback function when Box SDK refreshes tokens"""
            # Use keyring to store the tokens
            keyring.set_password('Box_Auth', 'mybox@box.com', access_token)
            if refresh_token is None:
                refresh_token = ""
            keyring.set_password('Box_Refresh', 'mybox@box.com', refresh_token)
    

    A caveat -- like I said, I don't know these APIs -- neither the box one, nor the keyring one you're using. But based on the code that's there, doing something like that sounds like it's well worth a try.