|
- # Copyright 2017 Gehirn Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
-
- import json
- from datetime import (
- datetime,
- timezone,
- )
- from typing import (
- AbstractSet,
- Any,
- Optional,
- )
-
- from jwt.utils import (
- get_time_from_int,
- )
-
- from .exceptions import (
- JWSDecodeError,
- JWSEncodeError,
- JWTDecodeError,
- JWTEncodeError,
- )
- from .jwk import AbstractJWKBase
- from .jws import JWS
-
-
- class JWT:
-
- def __init__(self):
- self._jws = JWS()
-
- def encode(
- self,
- payload: dict[str, Any],
- key: Optional[AbstractJWKBase] = None,
- alg="HS256",
- optional_headers: Optional[dict[str, str]] = None,
- ) -> str:
- if not isinstance(self, JWT): # pragma: no cover
- # https://github.com/GehirnInc/python-jwt/issues/15
- raise RuntimeError(
- "encode must be called on a jwt.JWT() instance. "
- "Do jwt.JWT().encode(...)"
- )
- if not isinstance(payload, dict): # pragma: no cover
- raise TypeError("payload must be a dict")
- if not (
- key is None or isinstance(key, AbstractJWKBase)
- ): # pragma: no cover
- raise TypeError(
- "key must be an instance of a class implements "
- "jwt.AbstractJWKBase"
- )
- if not (
- optional_headers is None or isinstance(optional_headers, dict)
- ): # pragma: no cover
- raise TypeError("optional_headers must be a dict")
-
- try:
- message = json.dumps(payload).encode("utf-8")
- except ValueError as why:
- raise JWTEncodeError(
- "payload must be able to be encoded to JSON"
- ) from why
-
- optional_headers = optional_headers and optional_headers.copy() or {}
- optional_headers["typ"] = "JWT"
- try:
- return self._jws.encode(message, key, alg, optional_headers)
- except JWSEncodeError as why:
- raise JWTEncodeError("failed to encode to JWT") from why
-
- def decode(
- self,
- message: str,
- key: Optional[AbstractJWKBase] = None,
- do_verify=True,
- algorithms: Optional[AbstractSet[str]] = None,
- do_time_check: bool = True,
- ) -> dict[str, Any]:
- if not isinstance(self, JWT): # pragma: no cover
- # https://github.com/GehirnInc/python-jwt/issues/15
- raise RuntimeError(
- "decode must be called on a jwt.JWT() instance. "
- "Do jwt.JWT().decode(...)"
- )
- if not isinstance(message, str): # pragma: no cover
- raise TypeError("message must be a str")
- if not (
- key is None or isinstance(key, AbstractJWKBase)
- ): # pragma: no cover
- raise TypeError(
- "key must be an instance of a class implements "
- "jwt.AbstractJWKBase"
- )
-
- # utc now with timezone
- now = datetime.now(timezone.utc)
- try:
- message_bin = self._jws.decode(message, key, do_verify, algorithms)
- except JWSDecodeError as why:
- raise JWTDecodeError("failed to decode JWT") from why
- try:
- payload = json.loads(message_bin.decode("utf-8"))
- except ValueError as why:
- raise JWTDecodeError(
- "a payload of the JWT is not valid JSON"
- ) from why
-
- # The "exp" (expiration time) claim identifies the expiration time on
- # or after which the JWT MUST NOT be accepted for processing.
- if "exp" in payload and do_time_check:
- try:
- exp = get_time_from_int(payload["exp"])
- except TypeError:
- raise JWTDecodeError("Invalid Expired value")
- if now >= exp:
- raise JWTDecodeError("JWT Expired")
-
- # The "nbf" (not before) claim identifies the time before which the JWT
- # MUST NOT be accepted for processing.
- if "nbf" in payload and do_time_check:
- try:
- nbf = get_time_from_int(payload["nbf"])
- except TypeError:
- raise JWTDecodeError('Invalid "Not valid yet" value')
- if now < nbf:
- raise JWTDecodeError("JWT Not valid yet")
-
- return payload
|