Skip to content

Commit 7ec089f

Browse files
authored
Merge pull request #149 from jschlyter/unpack_check_nbf_exp
JWT check nbf/exp
2 parents 361ecaa + 9ff84a2 commit 7ec089f

File tree

2 files changed

+106
-16
lines changed

2 files changed

+106
-16
lines changed

src/cryptojwt/jwt.py

+31-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""Basic JSON Web Token implementation."""
22
import json
33
import logging
4+
import time
45
import uuid
5-
from datetime import datetime
6-
from datetime import timezone
76
from json import JSONDecodeError
87

98
from .exception import HeaderError
@@ -28,9 +27,7 @@ def utc_time_sans_frac():
2827
2928
:return: A number of seconds
3029
"""
31-
32-
now_timestampt = int(datetime.now(timezone.utc).timestamp())
33-
return now_timestampt
30+
return int(time.time())
3431

3532

3633
def pick_key(keys, use, alg="", key_type="", kid=""):
@@ -95,6 +92,7 @@ def __init__(
9592
allowed_sign_algs=None,
9693
allowed_enc_algs=None,
9794
allowed_enc_encs=None,
95+
allowed_max_lifetime=None,
9896
zip="",
9997
):
10098
self.key_jar = key_jar # KeyJar instance
@@ -115,6 +113,7 @@ def __init__(
115113
self.allowed_sign_algs = allowed_sign_algs
116114
self.allowed_enc_algs = allowed_enc_algs
117115
self.allowed_enc_encs = allowed_enc_encs
116+
self.allowed_max_lifetime = allowed_max_lifetime
118117
self.zip = zip
119118

120119
def receiver_keys(self, recv, use):
@@ -176,13 +175,13 @@ def put_together_aud(recv, aud=None):
176175

177176
return _aud
178177

179-
def pack_init(self, recv, aud):
178+
def pack_init(self, recv, aud, iat=None):
180179
"""
181180
Gather initial information for the payload.
182181
183182
:return: A dictionary with claims and values
184183
"""
185-
argv = {"iss": self.iss, "iat": utc_time_sans_frac()}
184+
argv = {"iss": self.iss, "iat": iat or utc_time_sans_frac()}
186185
if self.lifetime:
187186
argv["exp"] = argv["iat"] + self.lifetime
188187

@@ -207,7 +206,7 @@ def pack_key(self, issuer_id="", kid=""):
207206

208207
return keys[0] # Might be more then one if kid == ''
209208

210-
def pack(self, payload=None, kid="", issuer_id="", recv="", aud=None, **kwargs):
209+
def pack(self, payload=None, kid="", issuer_id="", recv="", aud=None, iat=None, **kwargs):
211210
"""
212211
213212
:param payload: Information to be carried as payload in the JWT
@@ -216,13 +215,14 @@ def pack(self, payload=None, kid="", issuer_id="", recv="", aud=None, **kwargs):
216215
:param recv: The intended immediate receiver
217216
:param aud: Intended audience for this JWS/JWE, not expected to
218217
contain the recipient.
218+
:param iat: Override issued at (default current timestamp)
219219
:param kwargs: Extra keyword arguments
220220
:return: A signed or signed and encrypted Json Web Token
221221
"""
222222
_args = {}
223223
if payload is not None:
224224
_args.update(payload)
225-
_args.update(self.pack_init(recv, aud))
225+
_args.update(self.pack_init(recv, aud, iat))
226226

227227
try:
228228
_encrypt = kwargs["encrypt"]
@@ -304,11 +304,12 @@ def verify_profile(msg_cls, info, **kwargs):
304304
raise VerificationError()
305305
return _msg
306306

307-
def unpack(self, token):
307+
def unpack(self, token, timestamp=None):
308308
"""
309309
Unpack a received signed or signed and encrypted Json Web Token
310310
311311
:param token: The Json Web Token
312+
:param timestamp: Time for evaluation (default now)
312313
:return: If decryption and signature verification work the payload
313314
will be returned as a Message instance if possible.
314315
"""
@@ -378,6 +379,26 @@ def unpack(self, token):
378379
except KeyError:
379380
_msg_cls = None
380381

382+
timestamp = timestamp or utc_time_sans_frac()
383+
384+
if "nbf" in _info:
385+
nbf = int(_info["nbf"])
386+
if timestamp < nbf - self.skew:
387+
raise VerificationError("Token not yet valid")
388+
389+
if "exp" in _info:
390+
exp = int(_info["exp"])
391+
if timestamp >= exp + self.skew:
392+
raise VerificationError("Token expired")
393+
else:
394+
exp = None
395+
396+
if "iat" in _info:
397+
iat = int(_info["iat"])
398+
if self.allowed_max_lifetime and exp:
399+
if abs(exp - iat) > self.allowed_max_lifetime:
400+
raise VerificationError("Token lifetime exceeded")
401+
381402
if _msg_cls:
382403
vp_args = {"skew": self.skew}
383404
if self.iss:

tests/test_09_jwt.py

+75-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from cryptojwt.exception import IssuerNotFound
66
from cryptojwt.jws.exception import NoSuitableSigningKeys
77
from cryptojwt.jwt import JWT
8+
from cryptojwt.jwt import VerificationError
89
from cryptojwt.jwt import pick_key
10+
from cryptojwt.jwt import utc_time_sans_frac
911
from cryptojwt.key_bundle import KeyBundle
1012
from cryptojwt.key_jar import KeyJar
1113
from cryptojwt.key_jar import init_key_jar
@@ -81,15 +83,82 @@ def test_jwt_pack_and_unpack():
8183
assert set(info.keys()) == {"iat", "iss", "sub"}
8284

8385

84-
def test_jwt_pack_and_unpack_unknown_issuer():
86+
def test_jwt_pack_and_unpack_valid():
8587
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256")
88+
t = utc_time_sans_frac()
89+
payload = {"sub": "sub", "nbf": t, "exp": t + 3600}
90+
_jwt = alice.pack(payload=payload)
91+
92+
bob = JWT(key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"])
93+
info = bob.unpack(_jwt)
94+
95+
assert set(info.keys()) == {"iat", "iss", "sub", "nbf", "exp"}
96+
97+
98+
def test_jwt_pack_and_unpack_not_yet_valid():
99+
lifetime = 3600
100+
skew = 15
101+
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
102+
timestamp = utc_time_sans_frac()
103+
payload = {"sub": "sub", "nbf": timestamp}
104+
_jwt = alice.pack(payload=payload)
105+
106+
bob = JWT(key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"], skew=skew)
107+
_ = bob.unpack(_jwt, timestamp=timestamp - skew)
108+
with pytest.raises(VerificationError):
109+
_ = bob.unpack(_jwt, timestamp=timestamp - skew - 1)
110+
111+
112+
def test_jwt_pack_and_unpack_expired():
113+
lifetime = 3600
114+
skew = 15
115+
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
86116
payload = {"sub": "sub"}
87117
_jwt = alice.pack(payload=payload)
88118

89-
kj = KeyJar()
90-
bob = JWT(key_jar=kj, iss=BOB, allowed_sign_algs=["RS256"])
91-
with pytest.raises(IssuerNotFound):
92-
info = bob.unpack(_jwt)
119+
bob = JWT(key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"], skew=skew)
120+
iat = bob.unpack(_jwt)["iat"]
121+
_ = bob.unpack(_jwt, timestamp=iat + lifetime + skew - 1)
122+
with pytest.raises(VerificationError):
123+
_ = bob.unpack(_jwt, timestamp=iat + lifetime + skew)
124+
125+
126+
def test_jwt_pack_and_unpack_max_lifetime_exceeded():
127+
lifetime = 3600
128+
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
129+
payload = {"sub": "sub"}
130+
_jwt = alice.pack(payload=payload)
131+
132+
bob = JWT(
133+
key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"], allowed_max_lifetime=lifetime - 1
134+
)
135+
with pytest.raises(VerificationError):
136+
_ = bob.unpack(_jwt)
137+
138+
139+
def test_jwt_pack_and_unpack_max_lifetime_exceeded():
140+
lifetime = 3600
141+
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
142+
payload = {"sub": "sub"}
143+
_jwt = alice.pack(payload=payload)
144+
145+
bob = JWT(
146+
key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"], allowed_max_lifetime=lifetime - 1
147+
)
148+
with pytest.raises(VerificationError):
149+
_ = bob.unpack(_jwt)
150+
151+
152+
def test_jwt_pack_and_unpack_timestamp():
153+
lifetime = 3600
154+
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
155+
payload = {"sub": "sub"}
156+
_jwt = alice.pack(payload=payload, iat=42)
157+
158+
bob = JWT(key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"])
159+
_ = bob.unpack(_jwt, timestamp=42)
160+
with pytest.raises(VerificationError):
161+
_ = bob.unpack(_jwt)
93162

94163

95164
def test_jwt_pack_and_unpack_unknown_key():
@@ -261,4 +330,4 @@ def test_eddsa_jwt():
261330
kj = KeyJar()
262331
kj.add_kb(ISSUER, KeyBundle(JWKS_DICT))
263332
jwt = JWT(key_jar=kj)
264-
_ = jwt.unpack(JWT_TEST)
333+
_ = jwt.unpack(JWT_TEST, timestamp=1655278809)

0 commit comments

Comments
 (0)