Skip to content

Commit 4805ebf

Browse files
committed
Add model for JWT refresh token
1 parent 7d98f43 commit 4805ebf

File tree

5 files changed

+109
-0
lines changed

5 files changed

+109
-0
lines changed

changelog.d/3268.added.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add database model for JWT refresh tokens

python/nav/models/api.py

+25
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from nav.models.fields import VarcharField
2525
from nav.models.profiles import Account
26+
from nav.web.jwtgen import is_active
2627

2728

2829
class APIToken(models.Model):
@@ -66,3 +67,27 @@ def get_absolute_url(self):
6667

6768
class Meta(object):
6869
db_table = 'apitoken'
70+
71+
72+
class JWTRefreshToken(models.Model):
73+
74+
name = VarcharField(unique=True)
75+
description = models.TextField(null=True, blank=True)
76+
expires = models.DateTimeField()
77+
activates = models.DateTimeField()
78+
last_used = models.DateTimeField(null=True)
79+
revoked = models.BooleanField(default=False)
80+
hash = VarcharField()
81+
82+
def __str__(self):
83+
return self.name
84+
85+
def is_active(self) -> bool:
86+
"""Returns True if the token is active. A token is considered active when
87+
`expires` is in the future and `activates` is in the past or matches
88+
the current time.
89+
"""
90+
return is_active(self.expires.timestamp(), self.activates.timestamp())
91+
92+
class Meta(object):
93+
db_table = 'jwtrefreshtoken'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE manage.JWTRefreshToken (
2+
id SERIAL PRIMARY KEY,
3+
name VARCHAR NOT NULL UNIQUE,
4+
description VARCHAR,
5+
expires TIMESTAMP NOT NULL,
6+
activates TIMESTAMP NOT NULL,
7+
last_used TIMESTAMP,
8+
revoked BOOLEAN NOT NULL DEFAULT FALSE,
9+
hash VARCHAR NOT NULL
10+
);

python/nav/web/jwtgen.py

+15
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ def _generate_token(
4949
new_token, JWTConf().get_nav_private_key(), algorithm="RS256"
5050
)
5151
return encoded_token
52+
53+
54+
def is_active(exp: float, nbf: float) -> bool:
55+
"""
56+
Takes `exp` and `nbf` as POSIX timestamps. These represent the claims of a JWT token.
57+
`exp` should be the expiration time of the token and `nbf` should be the time when
58+
the token becomes active.
59+
60+
Returns True if `exp` is in the future and `nbf` is in the past or matches
61+
the current time.
62+
"""
63+
now = datetime.now(timezone.utc)
64+
expires = datetime.fromtimestamp(exp, tz=timezone.utc)
65+
activates = datetime.fromtimestamp(nbf, tz=timezone.utc)
66+
return now >= activates and now < expires
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from datetime import datetime, timedelta
2+
3+
from nav.models.api import JWTRefreshToken
4+
5+
6+
class TestIsActive:
7+
def test_should_return_false_if_token_activates_in_the_future(self):
8+
now = datetime.now()
9+
token = JWTRefreshToken(
10+
name="testtoken",
11+
hash="dummyhash",
12+
expires=now + timedelta(hours=1),
13+
activates=now + timedelta(hours=1),
14+
)
15+
assert not token.is_active()
16+
17+
def test_should_return_false_if_token_expires_in_the_past(self):
18+
now = datetime.now()
19+
token = JWTRefreshToken(
20+
name="testtoken",
21+
hash="dummyhash",
22+
expires=now - timedelta(hours=1),
23+
activates=now - timedelta(hours=1),
24+
)
25+
assert not token.is_active()
26+
27+
def test_should_return_true_if_token_activates_in_the_past_and_expires_in_the_future(
28+
self,
29+
):
30+
now = datetime.now()
31+
token = JWTRefreshToken(
32+
name="testtoken",
33+
hash="dummyhash",
34+
expires=now + timedelta(hours=1),
35+
activates=now - timedelta(hours=1),
36+
)
37+
assert token.is_active()
38+
39+
def test_should_return_true_if_token_activates_now_and_expires_in_the_future(self):
40+
now = datetime.now()
41+
token = JWTRefreshToken(
42+
name="testtoken",
43+
hash="dummyhash",
44+
expires=now + timedelta(hours=1),
45+
activates=now,
46+
)
47+
assert token.is_active()
48+
49+
50+
def test_string_representation_should_match_name():
51+
now = datetime.now()
52+
token = JWTRefreshToken(
53+
name="testtoken",
54+
hash="dummyhash",
55+
expires=now + timedelta(hours=1),
56+
activates=now - timedelta(hours=1),
57+
)
58+
assert str(token) == token.name

0 commit comments

Comments
 (0)