Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix type annotation for decode()/encode()'s parameter key #605

Closed
wants to merge 2 commits into from

Conversation

michael-k
Copy link
Contributor

@michael-k michael-k commented Jan 12, 2021

See #602 for details.

The issue suggests that the parameter key should not accept str. I've not included this for backwards compatibility and because I'm not sure if that's really necessary.

Closes #602

@jpadilla
Copy link
Owner

@jdufresne since you worked in some of the typing stuff recently, thoughts on this change?

Copy link
Contributor

@jdufresne jdufresne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there existing tests that cover type bytes? If not, they should be added.

jwt/api_jws.py Outdated Show resolved Hide resolved
CHANGELOG.rst Outdated Show resolved Hide resolved
@michael-k michael-k force-pushed the key-type branch 3 times, most recently from 9604b18 to d604ee7 Compare January 18, 2021 16:39
jwt/algorithms.py Outdated Show resolved Hide resolved
@WolfgangFellger
Copy link

Please note that there are even more types that some of the algorithms accept for the key argument.

@michael-k
Copy link
Contributor Author

michael-k commented Jan 20, 2021

Please note that there are even more types that some of the algorithms accept for the key argument.

I have no idea how to properly use them in the type annotations. Everything I've tried so far resulted some error by mypy.

This should work but would probably result in Union[Any, …] for every setup that doesn't use cryptography:

if MYPY:
    from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey

def prepare(key: Union[str, bytes, "EllipticCurvePrivateKey"]):
    pass

@WolfgangFellger
Copy link

Personally I would be okay with just reverting to Any.

@terencehonles
Copy link

terencehonles commented Feb 2, 2021

Please note that there are even more types that some of the algorithms accept for the key argument.

I have no idea how to properly use them in the type annotations. Everything I've tried so far resulted some error by mypy.

This should work but would probably result in Union[Any, …] for every setup that doesn't use cryptography:

if MYPY:
    from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey

def prepare(key: Union[str, bytes, "EllipticCurvePrivateKey"]):
    pass

I'm just checking to see if this relates to a change I was going to make with decode not accepting bytes, but the quoted Python should be expressed as the following:

# so KEY_TYPES is not needed at runtime and doesn't break because it's in the TYPE_CHECKING block.
# The constant could be define outside of the block and this future import would not be needed
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    try:
        from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
        KEY_TYPES = Union[str, bytes, EllipticCurvePrivateKey]
    except ImportError:
        KEY_TYPES = Union[str, bytes]

def prepare(key: KEY_TYPES): ...

If you need this to be dynamic you could do the following:

_KEY_TYPES = [str, bytes]

# import 1
_KEY_TYPES.append(...)
# import 2
_KEY_TYPES.append(...)

KEY_TYPES = Union[tuple(_KEY_TYPES)]

This works because obj[one, two] is just passing (one, two) to obj.__getitem__ and as long as the type is a tuple not a list it will be the expected union.

@terencehonles
Copy link

It looks like this change is adjacent, but not the change I was looking to make. jwt should also be bytes | str for the decode functions and there may be other places it's been made too specific and only accepts str.

@michael-k
Copy link
Contributor Author

Your first example leads to Cannot assign multiple types to name "KeyType" without an explicit "Type[...]" annotation.

KEY_TYPES = Union[tuple(_KEY_TYPES)]Invalid type alias: expression is not a valid type.

Also I don't think that something like _KEY_TYPES.append(…) could work at all as mypy doesn't import and/or run the code afaik. The same should be true for try-except.

@terencehonles
Copy link

terencehonles commented Feb 4, 2021

Hmm good point. I hadn't completely thought about that and I was going off what I did (a while ago) without testing mypy. At this point I've changed the original type declaration and am playing with getting mypy running so I mainly came here for #605 (comment) but if you're not able to import or type against a concrete type I'm assuming you'd need to type with a protocol, but that only comes in Python 3.8.

One thing to call out that without cryptography being declared as typed or on typeshed (I haven't checked if it is) the import will fail in mypy and it will fall back to Any.

It looks like it's on typeshed but it might make sense to declare a common base for private keys https://github.com/python/typeshed/blob/master/stubs/cryptography/cryptography/hazmat/primitives/asymmetric/ec.pyi so that way protocols could be used before Python 3.8. It might be possible to type with Protocols keeping everything behind if typing.TYPE_CHECKING and using from __future__ import annotations to make sure that any use of Protocols is by mypy on Python 3.8 or greater.

Anyways, it does sound like it should just be typed as Any.

@terencehonles
Copy link

It looks like Protocol would be supported pre Python 3.8 via the typing_extensions, but not sure if declaring the protocol would be worth it vs just typing with Any

@michael-k
Copy link
Contributor Author

@jdufresne any suggestions on how to proceed?

@terencehonles
Copy link

I believe the consensus here would be to use Any. I know you're looking for input from @jdufresne / @jpadilla 's input, but looking at the call chain a call to j*.decode it will call j*.decode_complete which will likely use the key for:

pyjwt/jwt/api_jws.py

Lines 217 to 224 in 587997e

def _verify_signature(
self,
signing_input,
header,
signature,
key="",
algorithms=None,
):
which is untyped (Any)

Looking at the API closer it looks like algorithms are puggable here pyjwt.jwt.algorithms which means the API cannot be type safe. A algorithm will alg.prepare_key(self, key: T) -> U and alg.verify(self, msg: ?, key: U, sig: ?) -> bool and there's not a good way to type both of those.

@dajiaji
Copy link
Contributor

dajiaji commented Mar 26, 2021

@michael-k @jpadilla I think this PR should be changed to use Any, and moved forward.

At present, even the simple sample codes in the specification gives mypy warnings.

@michael-k
Copy link
Contributor Author

I've changed the type annotation to Any. @jdufresne could you review this PR again?

@@ -76,7 +76,7 @@ def get_algorithms(self):
def encode(
self,
payload: bytes,
key: str,
key: Any,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you set a default value for encode as well as decode ?

Suggested change
key: Any,
key: Any = b"",

Copy link
Contributor Author

@michael-k michael-k Mar 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This PR doesn't set any default value, it just changes the one for decode.
  2. The suggested change would lead to “insecure by default”

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The omission of the key does not result in signing with the empty string as the private key, and from the perspective of consistency of API design, my suggestion has some validity. But, anyway, I understood. You don't need to fix it.

@@ -37,7 +37,7 @@ def _get_default_options() -> Dict[str, Union[bool, List[str]]]:
def encode(
self,
payload: Dict[str, Any],
key: str,
key: Any,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
key: Any,
key: Any = b"",

@dajiaji
Copy link
Contributor

dajiaji commented Mar 31, 2021

@michael-k One last thing: I think you should update https://github.com/jpadilla/pyjwt/blob/master/docs/api.rst.

Copy link
Collaborator

@auvipy auvipy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, please rebase

@github-actions github-actions bot added the stale Issues without activity for more than 60 days label Jun 8, 2022
@github-actions github-actions bot closed this Jun 15, 2022
@ps-george
Copy link

bump - this is still an issue

@michael-k
Copy link
Contributor Author

I've rebased, but I don't seem to be able to re-open this PR.

@lcy0321
Copy link

lcy0321 commented Dec 4, 2022

Hi @michael-k, @auvipy,
I think this is still an issue, will this PR be re-opened?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Issues without activity for more than 60 days
Projects
None yet
Development

Successfully merging this pull request may close these issues.

encode()/decode() key parameter type should not be str
9 participants