diff --git a/changelog.d/+.added.md b/changelog.d/+.added.md new file mode 100644 index 0000000000..0084758571 --- /dev/null +++ b/changelog.d/+.added.md @@ -0,0 +1 @@ +Add database model for JWT refresh tokens diff --git a/python/nav/models/api.py b/python/nav/models/api.py index d3ac6213c9..a546e4a4b7 100644 --- a/python/nav/models/api.py +++ b/python/nav/models/api.py @@ -15,11 +15,12 @@ # """Models for the NAV API""" -from datetime import datetime +from datetime import datetime, timezone from django.contrib.postgres.fields import HStoreField from django.db import models from django.urls import reverse +from django.db.models import JSONField from nav.models.fields import VarcharField from nav.models.profiles import Account @@ -66,3 +67,26 @@ def get_absolute_url(self): class Meta(object): db_table = 'apitoken' + + +class JWTRefreshToken(models.Model): + + name = VarcharField(unique=True) + description = models.TextField(null=True, blank=True) + data = JSONField() + hash = VarcharField() + + def __str__(self): + return self.name + + def is_active(self) -> bool: + """True if token is active. A token is considered active when + the nbf claim is in the past and the exp claim is in the future + """ + now = datetime.now(tz=timezone.utc) + nbf = datetime.fromtimestamp(self.data['nbf'], tz=timezone.utc) + exp = datetime.fromtimestamp(self.data['exp'], tz=timezone.utc) + return now >= nbf and now < exp + + class Meta(object): + db_table = 'jwtrefreshtoken' diff --git a/python/nav/models/sql/changes/sc.05.13.0001.sql b/python/nav/models/sql/changes/sc.05.13.0001.sql new file mode 100644 index 0000000000..f95f3ff775 --- /dev/null +++ b/python/nav/models/sql/changes/sc.05.13.0001.sql @@ -0,0 +1,7 @@ +CREATE TABLE manage.JWTRefreshToken ( + id SERIAL PRIMARY KEY, + data JSON NOT NULL, + name VARCHAR NOT NULL UNIQUE, + description VARCHAR, + hash VARCHAR NOT NULL +); diff --git a/tests/integration/models/jwtrefreshtoken_test.py b/tests/integration/models/jwtrefreshtoken_test.py new file mode 100644 index 0000000000..f32d188da8 --- /dev/null +++ b/tests/integration/models/jwtrefreshtoken_test.py @@ -0,0 +1,53 @@ +import pytest +from datetime import datetime, timedelta, timezone + +from nav.models.api import JWTRefreshToken + + +class TestIsActive: + def test_should_return_false_if_nbf_is_in_the_future(self, token): + now = datetime.now(tz=timezone.utc) + token.data['nbf'] = (now + timedelta(hours=1)).timestamp() + token.data['exp'] = (now + timedelta(hours=1)).timestamp() + assert not token.is_active() + + def test_should_return_false_if_exp_is_in_the_past(self, token): + now = datetime.now(tz=timezone.utc) + token.data['nbf'] = (now - timedelta(hours=1)).timestamp() + token.data['exp'] = (now - timedelta(hours=1)).timestamp() + assert not token.is_active() + + def test_should_return_true_if_nbf_is_in_the_past_and_exp_is_in_the_future( + self, token + ): + now = datetime.now(tz=timezone.utc) + token.data['nbf'] = (now - timedelta(hours=1)).timestamp() + token.data['exp'] = (now + timedelta(hours=1)).timestamp() + assert token.is_active() + + +def test_string_representation_should_match_token(token): + assert str(token) == token.name + + +@pytest.fixture() +def token(data) -> JWTRefreshToken: + return JWTRefreshToken( + name="testtoken", + description="this is a test token", + data=data, + hash="dummyhash", + ) + + +@pytest.fixture() +def data() -> dict: + data = { + "exp": 1516339022, + "nbf": 1516239022, + "iat": 1516239022, + "aud": "nav", + "iss": "nav", + "token_type": "refresh_token", + } + return data