1

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?

Arne
  • 17,706
  • 5
  • 83
  • 99
  • Perhaps you can try it backward - decode the undesired token and see what `json` object returns to work your way back. At a glance a few possibilities exists, including that `id=1` maybe interpreted as an `int` instead of a `str` as you expected. – r.ook Oct 30 '18 at 12:56
  • Good call, I hadn't tried that yet. But it doesn't work, it prints in json format and won't take it back. – Arne Oct 30 '18 at 13:11
  • This is the object it returned: `{'nested': '[{name=me,', 'id': '1}]'}`, it would appear anything immediately following `nested=...` is taken verbatim as a string as the value of nested. The extended CLI help on the `pyjwt encode` isn't that helpful either. Worst case scenario, nested objects might not be supported in CLI. – r.ook Oct 30 '18 at 13:13
  • Seems to me like it isn't. I opened [an issue](https://github.com/jpadilla/pyjwt/issues/380) with them. – Arne Oct 30 '18 at 13:27

1 Answers1

1

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.

r.ook
  • 13,466
  • 2
  • 22
  • 39
  • Nice! I think I'll take option #1, since it's code that's gonna be run in a CI/CD pipeline. Creating scripts on the fly or changing constantly re-installed packages on each run would be worse, I think. That being said, you could post your code as a PR to the issue I opened. – Arne Oct 30 '18 at 14:51
  • I don't really use github much (shocker) as I'm more a hobbist, but feel free to add all/any part of my answer to your issue/PR to help the community. – r.ook Oct 30 '18 at 15:07
  • 1
    Alright, I might just do that. You will get proper credits for your troubles though, and thanks again for the help! – Arne Oct 30 '18 at 15:12