Search code examples
pythonjwtpyjwt

How to use lists or dicts as command line arguments in pyJWT


The following python code produces a valid JWT token, using pyjwt:

>>> import jwt
>>> payload = {'nested': [{'name': 'me', 'id': '1'}]}
>>> token = jwt.encode(payload, 'secret')
>>> token.decode()
ey[...]ko0Zq_k

pyjwt also supports calls from the command line interface. But the docs only show examples with = separated key value pairs and not with nested payloads.

My best guess was this:

$ pyjwt --key=secret encode nested=[{name=me, id=1}]
ey[...]0FRW9gyU  # not the same token as above :(

Which didn't work. Is it simply not supported?


Solution

  • As mentioned, your command line token when decoded returns this json object:

    {'nested': '[{name=me,', 'id': '1}]'}

    A quick dive into the __main__.py of jwt package gives this little snippet:

    ... snipped
    
    def encode_payload(args):
        # Try to encode
        if args.key is None:
            raise ValueError('Key is required when encoding. See --help for usage.')
    
        # Build payload object to encode
        payload = {}
    
        for arg in args.payload:
            k, v = arg.split('=', 1)
    
        ... some additional handling on v for time, int, float and True/False/None
        ... snipped
    

    As you can see the key and value of the payload is determined directly based on the split('=', 1), so it anything passed the first = in your command line following a key will always be determined as a single value (with some conversion afterwards).

    So in short, nested dicts in CLI is not supported.

    However, the semi-good news is, there are certain ways you can work around these:

    1. Run an impromptu statement off Python's CLI directly like so:

      > python -c "import jwt; print(jwt.encode({'nested':[{'name':'me', 'id':'1'}]}, 'secret').decode('utf-8'))"
      
      # eyJ...Zq_k
      

    Not exactly ideal, but it gives you what you need.

    1. Save the same script into a .py capable of taking args and execute it on Python's CLI:

      import sys, jwt
      my_json = sys.argv[0]
      token = jwt.encode(eval(my_json), 'secret')
      print(token.decode('utf-8'))
      
      # run in CLI
      > python my_encode.py "{'nested':[{'name':'me', 'id':'1'}]}"
      
      # eyJ...Zq_k
      

    Note the use of eval() here is not ideal because of security concerns. This is just my lazy way of implementing it because I don't want to write a parser for the args. If you absolutely must use CLI for your implementation and it's exposed, I would highly recommend you invest the effort into cleansing and parsing the argvs more carefully.

    1. The most contrived way: you can try to modify the Lib\site-packages\jwt\__main__.py function (at your own peril) to suit your need until official support is added. I'd caution you should be rather comfortable with writing your own parse though before considering messing with the main code. I took a few stab at it before I realize the limitations you will be running into:

      a. The main encode() method doesn't consider a list as a valid JSON object (but it should). So right off the bat you must have a dict like string to manipulate.

      b. The code always forces numbers to be cast as int or float if possible. You'll need to escape it somehow or entirely change the way it handle numbers.

      My attempt went something like this:

      def func(result, payload):
          for arg in payload:
              k, v = arg.split('=', 1)
      
              if v.startswith('{') and v.endswith('}'):
                  result[k] = func({}, v[1:-1])
              else:
              ... the rest of the existing code
      

      However I quickly ran into the limitation of the original arguments are already space delimited and assume it's a k, v pair, I would need to further handle another delimiter like , as well as capability to handle lists, and it could get messier. It's definitely doable, and the effect is immediate i.e. the CLI runs directly off of this __main__.py, but it's more work than I'd like to invest at the moment so I leave it with your capable hands.

    The effort to overcome these issues to achieve what you need might be more than necessary, depend on your skill and comfort level. So pick your battle... if CLI is not absolutely necessary, I'd suggest just use the .py methods instead.