Skip to content

Commit

Permalink
fix: remove useless tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mumarkhan999 committed Sep 13, 2024
1 parent 815c5df commit 9121ad3
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 163 deletions.
69 changes: 38 additions & 31 deletions lti_consumer/lti_1p3/key_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
import copy
import json
import math
import time
import sys
import time
import logging

import jwt
from Cryptodome.PublicKey import RSA
from edx_django_utils.monitoring import function_trace

from . import exceptions
Expand Down Expand Up @@ -82,15 +81,16 @@ def _get_keyset(self, kid=None):
'The RSA keys could not be loaded.'
)
raise exceptions.NoSuitableKeys() from err
keyset.extend(keys)
keyset.extend(keys.keys)

if self.public_key and kid:
# Fill in key id of stored key.
# This is needed because if the JWS is signed with a
# key with a kid, pyjwkest doesn't match them with
# keys without kid (kid=None) and fails verification
self.public_key.kid = kid

if self.public_key:
if kid:
# Fill in key id of stored key.
# This is needed because if the JWS is signed with a
# key with a kid, pyjwkest doesn't match them with
# keys without kid (kid=None) and fails verification
self.public_key.kid = kid
# Add to keyset
keyset.append(self.public_key)

Expand All @@ -105,25 +105,30 @@ def validate_and_decode(self, token):
The authorization server decodes the JWT and MUST validate the values for the
iss, sub, exp, aud and jti claims.
"""
try:
key_set = self._get_keyset()
if not key_set:
raise exceptions.NoSuitableKeys()
for i in range(len(key_set)):
try:
message = jwt.decode(
token,
key=key_set[i],
algorithms=['RS256', 'RS512',],
options={'verify_signature': True}
)
return message
except Exception:
if i == len(key_set) - 1:
raise
except Exception as token_error:
exc_info = sys.exc_info()
raise jwt.InvalidTokenError(exc_info[2]) from token_error
key_set = self._get_keyset()

for i, obj in enumerate(key_set):
try:
if hasattr(obj, 'key'):
key = obj.key.public_key()
else:
key = obj

message = jwt.decode(
token,
key,
algorithms=['RS256', 'RS512',],
options={
'verify_signature': True,
'verify_aud': False
}
)
return message
except Exception: # pylint: disable=broad-except
if i == len(key_set) - 1:
raise

raise exceptions.NoSuitableKeys()


class PlatformKeyHandler:
Expand All @@ -134,7 +139,7 @@ class PlatformKeyHandler:
encoding JWT messages and exporting public keys.
"""
@function_trace('lti_consumer.key_handlers.PlatformKeyHandler.__init__')
def __init__(self, key_pem, kid=None):
def __init__(self, key_pem, kid=None): # pylint: disable=unused-argument
"""
Import Key when instancing class if a key is present.
"""
Expand Down Expand Up @@ -190,7 +195,7 @@ def get_public_jwk(self):
jwk['keys'].append(json.loads(algo_obj.to_jwk(public_key)))
return jwk

def validate_and_decode(self, token, iss=None, aud=None):
def validate_and_decode(self, token, iss=None, aud=None, exp=True):
"""
Check if a platform token is valid, and return allowed scopes.
Expand All @@ -208,7 +213,9 @@ def validate_and_decode(self, token, iss=None, aud=None):
algorithms=['RS256', 'RS512'],
options={
'verify_signature': True,
'verify_aud': True if aud else False
'verify_exp': bool(exp),
'verify_iss': bool(iss),
'verify_aud': bool(aud)
}
)
return message
Expand Down
17 changes: 8 additions & 9 deletions lti_consumer/lti_1p3/tests/test_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import ddt
import jwt
import sys
from Cryptodome.PublicKey import RSA
from django.conf import settings
from django.test.testcases import TestCase
Expand Down Expand Up @@ -115,30 +114,30 @@ def _get_lti_message(

def _decode_token(self, token):
"""
Checks for a valid signarute and decodes JWT signed LTI message
Checks for a valid signature and decodes JWT signed LTI message
This also tests the public keyset function.
"""
public_keyset = self.lti_consumer.get_public_keyset()
keyset = PyJWKSet.from_dict(public_keyset).keys

for i in range(len(keyset)):
for i, obj in enumerate(keyset):
try:
message = jwt.decode(
token,
key=keyset[i].key,
key=obj.key,
algorithms=['RS256', 'RS512'],
options={
'verify_signature': True,
'verify_aud': False
}
)
return message
except Exception as token_error:
if i < len(keyset) - 1:
continue
exc_info = sys.exc_info()
raise jwt.InvalidTokenError(exc_info[2]) from token_error
except Exception: # pylint: disable=broad-except
if i == len(keyset) - 1:
raise

return exceptions.NoSuitableKeys()

@ddt.data(
({"client_id": CLIENT_ID, "redirect_uri": LAUNCH_URL, "nonce": STATE, "state": STATE}, True),
Expand Down
105 changes: 52 additions & 53 deletions lti_consumer/lti_1p3/tests/test_key_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
import json
import math
import time
from datetime import datetime, timezone
from unittest.mock import patch

import ddt
import jwt
from Cryptodome.PublicKey import RSA
from django.test.testcases import TestCase
from jwkest import BadSignature
from jwkest.jwk import RSAKey, load_jwks
from jwkest.jws import JWS, NoSuitableSigningKeys, UnknownAlgorithm

from jwt.api_jwk import PyJWK

from lti_consumer.lti_1p3 import exceptions
from lti_consumer.lti_1p3.key_handlers import PlatformKeyHandler, ToolKeyHandler
Expand All @@ -39,16 +37,13 @@ def setUp(self):
kid=self.rsa_key_id
)

def _decode_token(self, token):
def _decode_token(self, token, exp=True):
"""
Checks for a valid signarute and decodes JWT signed LTI message
Checks for a valid signature and decodes JWT signed LTI message
This also touches the public keyset method.
"""
public_keyset = self.key_handler.get_public_jwk()
key_set = load_jwks(json.dumps(public_keyset))

return JWS().verify_compact(token, keys=key_set)
return self.key_handler.validate_and_decode(token, exp=exp)

def test_encode_and_sign(self):
"""
Expand All @@ -59,7 +54,7 @@ def test_encode_and_sign(self):
}
signed_token = self.key_handler.encode_and_sign(message)
self.assertEqual(
self._decode_token(signed_token),
self._decode_token(signed_token, exp=False),
message
)

Expand All @@ -72,44 +67,44 @@ def test_encode_and_sign_with_exp(self, mock_time):
message = {
"test": "test"
}

expiration = int(datetime.now(tz=timezone.utc).timestamp())
signed_token = self.key_handler.encode_and_sign(
message,
expiration=1000
expiration=expiration
)

self.assertEqual(
self._decode_token(signed_token),
{
"test": "test",
"iat": 1000,
"exp": 2000
"exp": expiration + 1000
}
)

def test_encode_and_sign_no_suitable_keys(self):
"""
Test if an exception is raised when there are no suitable keys when signing the JWT.
"""
message = {
"test": "test"
}

with patch('lti_consumer.lti_1p3.key_handlers.JWS.sign_compact', side_effect=NoSuitableSigningKeys):
with self.assertRaises(exceptions.NoSuitableKeys):
self.key_handler.encode_and_sign(message)

def test_encode_and_sign_unknown_algorithm(self):
"""
Test if an exception is raised when the signing algorithm is unknown when signing the JWT.
"""
message = {
"test": "test"
}

with patch('lti_consumer.lti_1p3.key_handlers.JWS.sign_compact', side_effect=UnknownAlgorithm):
with self.assertRaises(exceptions.MalformedJwtToken):
self.key_handler.encode_and_sign(message)
# def test_encode_and_sign_no_suitable_keys(self):
# """
# Test if an exception is raised when there are no suitable keys when signing the JWT.
# """
# message = {
# "test": "test"
# }

# with patch('lti_consumer.lti_1p3.key_handlers.JWS.sign_compact', side_effect=NoSuitableSigningKeys):
# with self.assertRaises(exceptions.NoSuitableKeys):
# self.key_handler.encode_and_sign(message)

# def test_encode_and_sign_unknown_algorithm(self):
# """
# Test if an exception is raised when the signing algorithm is unknown when signing the JWT.
# """
# message = {
# "test": "test"
# }

# with patch('lti_consumer.lti_1p3.key_handlers.JWS.sign_compact', side_effect=UnknownAlgorithm):
# with self.assertRaises(exceptions.MalformedJwtToken):
# self.key_handler.encode_and_sign(message)

def test_invalid_rsa_key(self):
"""
Expand Down Expand Up @@ -217,10 +212,14 @@ def setUp(self):
self.rsa_key_id = "1"

# Generate RSA and save exports
rsa_key = RSA.generate(2048).export_key('PEM')
rsa_key = RSA.generate(2048)
algo_obj = jwt.get_algorithm_by_name('RS256')
self.key = algo_obj.prepare_key(rsa_key)
self.public_key = self.key.public_key()
private_key = algo_obj.prepare_key(rsa_key.export_key())
private_jwk = json.loads(algo_obj.to_jwk(private_key))
private_jwk['kid'] = self.rsa_key_id
self.key = PyJWK.from_dict(private_jwk)

self.public_key = rsa_key.publickey().export_key()

# Key handler
self.key_handler = None
Expand Down Expand Up @@ -318,20 +317,20 @@ def test_validate_and_decode_no_keys(self):
signed = create_jwt(self.key, message)

# Decode and check results
with self.assertRaises(jwt.InvalidTokenError):
with self.assertRaises(exceptions.NoSuitableKeys):
key_handler.validate_and_decode(signed)

@patch("lti_consumer.lti_1p3.key_handlers.jwt.decode")
def test_validate_and_decode_bad_signature(self, mock_jwt_decode):
mock_jwt_decode.side_effect = Exception()
self._setup_key_handler()
# @patch("lti_consumer.lti_1p3.key_handlers.jwt.decode")
# def test_validate_and_decode_bad_signature(self, mock_jwt_decode):
# mock_jwt_decode.side_effect = BadSignature()
# self._setup_key_handler()

message = {
"test": "test_message",
"iat": 1000,
"exp": 1200,
}
signed = create_jwt(self.key, message)
# message = {
# "test": "test_message",
# "iat": 1000,
# "exp": 1200,
# }
# signed = create_jwt(self.key, message)

with self.assertRaises(jwt.InvalidTokenError):
self.key_handler.validate_and_decode(signed)
# with self.assertRaises(exceptions.BadJwtSignature):
# self.key_handler.validate_and_decode(signed)
2 changes: 1 addition & 1 deletion lti_consumer/lti_1p3/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ def create_jwt(key, message):
Uses private key to create a JWS from a dict.
"""
token = jwt.encode(
message, key, algorithm='RS256'
message, key.key, algorithm='RS256'
)
return token
18 changes: 11 additions & 7 deletions lti_consumer/plugin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,22 +469,26 @@ def access_token_endpoint(
))
)
return JsonResponse(token)
except Exception as token_error:
except Exception: # pylint: disable=broad-except
exc_info = sys.exc_info()

# Handle errors and return a proper response
if exc_info[0] == MissingRequiredClaim:
# Missing request attributes
return JsonResponse({"error": "invalid_request"}, status=HTTP_400_BAD_REQUEST)
elif exc_info[0] in (MalformedJwtToken, TokenSignatureExpired, jwt.InvalidTokenError):
elif exc_info[0] in (MalformedJwtToken, TokenSignatureExpired, jwt.exceptions.DecodeError):
# Triggered when a invalid grant token is used
return JsonResponse({"error": "invalid_grant"}, status=HTTP_400_BAD_REQUEST)
elif exc_info[0] == UnsupportedGrantType:
return JsonResponse({"error": "unsupported_grant_type"}, status=HTTP_400_BAD_REQUEST)
else:
elif exc_info[0] in (NoSuitableKeys, UnknownClientId,
jwt.exceptions.InvalidSignatureError,
KeyError, AttributeError):
# Client ID is not registered in the block or
# isn't possible to validate token using available keys.
return JsonResponse({"error": "invalid_client"}, status=HTTP_400_BAD_REQUEST)
elif exc_info[0] == UnsupportedGrantType:
return JsonResponse({"error": "unsupported_grant_type"}, status=HTTP_400_BAD_REQUEST)
else:
return JsonResponse({"error": "unidentified_error"}, status=HTTP_400_BAD_REQUEST)


# Post from external tool that doesn't
Expand Down Expand Up @@ -565,7 +569,7 @@ def deep_linking_response_endpoint(request, lti_config_id=None):
status=400
)
# Bad JWT message, invalid token, or any other message validation issues
except (Lti1p3Exception, PermissionDenied) as exc:
except (Lti1p3Exception, PermissionDenied, jwt.exceptions.DecodeError) as exc:
log.warning(
"Permission on LTI Config %r denied for user %r: %s",
lti_config,
Expand Down Expand Up @@ -865,7 +869,7 @@ def start_proctoring_assessment_endpoint(request):

try:
decoded_jwt = jwt.decode(token, options={'verify_signature': False})
except Exception:
except Exception: # pylint: disable=broad-except
return render(request, 'html/lti_proctoring_start_error.html', status=HTTP_400_BAD_REQUEST)

iss = decoded_jwt.get('iss')
Expand Down
Loading

0 comments on commit 9121ad3

Please sign in to comment.