Skip to content

Commit da4cb2f

Browse files
Merge pull request #2550 from IFRCGo/feat/jwt-monty
feat: update token algorithm
2 parents 16f7a2e + f6a7c43 commit da4cb2f

File tree

7 files changed

+63
-21
lines changed

7 files changed

+63
-21
lines changed

main/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
# Misc
123123
DISABLE_API_CACHE=(bool, False),
124124
# jwt private and public key (NOTE: Used algorithm ES256)
125+
# FIXME: Deprecated configuration. Remove this and it references
125126
JWT_PRIVATE_KEY_BASE64_ENCODED=(str, None),
126127
JWT_PUBLIC_KEY_BASE64_ENCODED=(str, None),
127128
JWT_PRIVATE_KEY=(str, None),
@@ -849,6 +850,8 @@ def decode_base64(env_key, fallback_env_key):
849850
AZURE_OPENAI_DEPLOYMENT_NAME = env("AZURE_OPENAI_DEPLOYMENT_NAME")
850851

851852
OIDC_ENABLE = env("OIDC_ENABLE")
853+
OIDC_RSA_PRIVATE_KEY = None
854+
OIDC_RSA_PUBLIC_KEY = None
852855
if OIDC_ENABLE:
853856
LOGIN_REDIRECT_URL = "go_login"
854857
LOGOUT_REDIRECT_URL = "go_login"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.19 on 2025-09-05 05:21
2+
3+
from django.db import migrations, models
4+
5+
6+
def set_is_old_token(apps, schema_editor):
7+
UserExternalToken = apps.get_model("registrations", "UserExternalToken")
8+
UserExternalToken.objects.update(is_old_token=True)
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
dependencies = [
14+
('registrations', '0011_userexternaltoken'),
15+
]
16+
17+
operations = [
18+
migrations.AddField(
19+
model_name='userexternaltoken',
20+
name='is_old_token',
21+
field=models.BooleanField(default=False, help_text='Marks whether this is an old Montandon token', verbose_name='is old token?'),
22+
),
23+
migrations.RunPython(set_is_old_token, reverse_code=migrations.RunPython.noop),
24+
]

registrations/models.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ class UserExternalToken(models.Model):
9191
jti = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, help_text=_("Unique identifier for the token"))
9292
created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
9393
expire_timestamp = models.DateTimeField(verbose_name=_("expire timestamp"))
94+
95+
# FIXME: Cleanup old token and remove this field
96+
is_old_token = models.BooleanField(
97+
verbose_name=_("is old token?"),
98+
default=False,
99+
help_text=_("Marks whether this is an old Montandon token"),
100+
)
94101
# @Note: Currently not used, but could be utilized for a blacklist feature.
95102
# is_disabled = models.BooleanField(verbose_name=_('is disabled?'), default=False)
96103

@@ -102,4 +109,9 @@ def __str__(self):
102109
return f'{self.title}-{self.expire_timestamp.strftime("%Y-%m-%d")}'
103110

104111
def get_payload(self) -> dict:
105-
return {"jti": str(self.jti), "userId": self.user_id, "exp": self.expire_timestamp, "inMovement": True}
112+
return {
113+
"jti": str(self.jti),
114+
"userId": self.user_id,
115+
"exp": self.expire_timestamp,
116+
"inMovement": True,
117+
}

registrations/serializers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,11 @@ def save(self):
145145
class UserExternalTokenSerializer(serializers.ModelSerializer):
146146
token = serializers.CharField(read_only=True)
147147
expire_timestamp = serializers.DateTimeField(required=False)
148+
is_old_token = serializers.BooleanField(read_only=True)
148149

149150
class Meta:
150151
model = UserExternalToken
151-
fields = ["title", "token", "expire_timestamp", "created_at"]
152+
fields = ["title", "token", "expire_timestamp", "created_at", "is_old_token"]
152153

153154
def validate_expire_timestamp(self, date):
154155
now = timezone.now()
@@ -169,7 +170,7 @@ def create(self, validated_data):
169170
validated_data["expire_timestamp"] = timezone.now() + timedelta(days=settings.JWT_EXPIRE_TIMESTAMP_DAYS)
170171

171172
# Check if private and public key exists
172-
if not (settings.JWT_PRIVATE_KEY and settings.JWT_PUBLIC_KEY):
173+
if not (settings.OIDC_RSA_PRIVATE_KEY and settings.OIDC_RSA_PUBLIC_KEY):
173174
raise serializers.ValidationError("Please contact system adminstrators to configurate private and public key.")
174175
instance = super().create(validated_data)
175176
validated_data["created_at"] = instance.created_at

registrations/test_views.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from cryptography.hazmat.backends import default_backend
1212
from cryptography.hazmat.primitives import serialization
13-
from cryptography.hazmat.primitives.asymmetric import ec
13+
from cryptography.hazmat.primitives.asymmetric import rsa
1414
from django.contrib.auth.models import User
1515
from django.test import override_settings
1616
from rest_framework.test import APITestCase
@@ -175,7 +175,7 @@ class UserExternalTokenTest(GoAPITestCase):
175175

176176
def setUp(self):
177177
super().setUp()
178-
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
178+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend())
179179
public_key = private_key.public_key()
180180
private_key_pem = private_key.private_bytes(
181181
encoding=serialization.Encoding.PEM,
@@ -185,11 +185,12 @@ def setUp(self):
185185

186186
# Serialize public key
187187
public_key_pem = public_key.public_bytes(
188-
encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
188+
encoding=serialization.Encoding.PEM,
189+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
189190
)
190191

191-
self.JWT_PRIVATE_KEY = private_key_pem.decode("utf-8")
192-
self.JWT_PUBLIC_KEY = public_key_pem.decode("utf-8")
192+
self.OIDC_RSA_PRIVATE_KEY = private_key_pem.decode("utf-8")
193+
self.OIDC_RSA_PUBLIC_KEY = public_key_pem.decode("utf-8")
193194

194195
def test_external_token_with_key(self):
195196
self.client.force_authenticate(self.user)
@@ -205,8 +206,8 @@ def test_external_token_with_key(self):
205206
data = {"title": "ok"}
206207

207208
with override_settings(
208-
JWT_PRIVATE_KEY=self.JWT_PRIVATE_KEY,
209-
JWT_PUBLIC_KEY=self.JWT_PUBLIC_KEY,
209+
OIDC_RSA_PRIVATE_KEY=self.OIDC_RSA_PRIVATE_KEY,
210+
OIDC_RSA_PUBLIC_KEY=self.OIDC_RSA_PUBLIC_KEY,
210211
):
211212
response = self.client.post("/api/v2/external-token/", data, format="json")
212213
self.assertEqual(response.status_code, 201)

registrations/utils.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.conf import settings
33
from django.contrib.auth.models import User
44
from django.db.models.functions import Lower
5+
from oauth2_provider.utils import jwk_from_pem
56

67
from api.models import Country, Profile, UserRegion
78

@@ -47,16 +48,12 @@ def getRegionalAdmins(userId):
4748

4849

4950
def jwt_encode_handler(payload):
51+
key = jwk_from_pem(settings.OIDC_RSA_PRIVATE_KEY)
5052
return jwt.encode(
5153
payload,
52-
settings.JWT_PRIVATE_KEY,
53-
algorithm="ES256",
54-
)
55-
56-
57-
def jwt_decode_handler(token):
58-
return jwt.decode(
59-
token,
60-
settings.JWT_PUBLIC_KEY,
61-
algorithms=["ES256"],
54+
settings.OIDC_RSA_PRIVATE_KEY,
55+
headers={
56+
"kid": key.thumbprint(),
57+
},
58+
algorithm="RS256",
6259
)

registrations/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,11 @@ class UserExternalTokenViewset(viewsets.ModelViewSet):
152152
]
153153

154154
def get_queryset(self):
155-
return UserExternalToken.objects.filter(user=self.request.user)
155+
return UserExternalToken.objects.filter(
156+
user=self.request.user,
157+
# NOTE: Hide old token from API
158+
is_old_token=False,
159+
)
156160

157161
def destroy(self, request, *args, **kwargs):
158162
return bad_request("Delete method not allowed")

0 commit comments

Comments
 (0)