44
55import json
66import logging
7- import urllib .parse
87from datetime import timedelta
98from unittest .mock import Mock , NonCallableMock , PropertyMock , patch
109
1312from django .conf import settings as dj_settings
1413from django .test .testcases import TestCase
1514from django .utils import timezone
16- from jwkest .jwk import RSAKey
15+ from jwkest .jwk import RSAKey , KEYS
1716
1817from lti_consumer .api import get_lti_1p3_launch_info
1918from lti_consumer .exceptions import LtiError
2019from lti_consumer .lti_1p3 .tests .utils import create_jwt
2120from lti_consumer .lti_xblock import LtiConsumerXBlock , parse_handler_suffix
2221from lti_consumer .tests .unit import test_utils
23- from lti_consumer .tests .unit .test_utils import FAKE_USER_ID , make_request , make_xblock
22+ from lti_consumer .tests .unit .test_utils import FAKE_USER_ID , make_jwt_request , make_request , make_xblock
2423from lti_consumer .utils import resolve_custom_parameter_template
2524
2625HTML_PROBLEM_PROGRESS = '<div class="problem-progress">'
@@ -452,6 +451,8 @@ def test_lti_1p3_fields_appear(self):
452451 'lti_version' ,
453452 'lti_1p3_launch_url' ,
454453 'lti_1p3_oidc_url' ,
454+ 'lti_1p3_tool_key_mode' ,
455+ 'lti_1p3_tool_keyset_url' ,
455456 'lti_1p3_tool_public_key' ,
456457 'lti_advantage_deep_linking_enabled' ,
457458 'lti_advantage_deep_linking_launch_url' ,
@@ -1484,17 +1485,7 @@ def test_access_token_malformed(self):
14841485 """
14851486 Test request with invalid JWT.
14861487 """
1487- request = make_request (
1488- urllib .parse .urlencode ({
1489- "grant_type" : "client_credentials" ,
1490- "client_assertion_type" : "something" ,
1491- "client_assertion" : "invalid-jwt" ,
1492- "scope" : "" ,
1493- }),
1494- 'POST'
1495- )
1496- request .content_type = 'application/x-www-form-urlencoded'
1497-
1488+ request = make_jwt_request ("invalid-jwt" )
14981489 response = self .xblock .lti_1p3_access_token (request )
14991490 self .assertEqual (response .status_code , 400 )
15001491 self .assertEqual (response .json_body , {'error' : 'invalid_grant' })
@@ -1503,15 +1494,7 @@ def test_access_token_invalid_grant(self):
15031494 """
15041495 Test request with invalid grant.
15051496 """
1506- request = make_request (
1507- urllib .parse .urlencode ({
1508- "grant_type" : "password" ,
1509- "client_assertion_type" : "something" ,
1510- "client_assertion" : "invalit-jwt" ,
1511- "scope" : "" ,
1512- }),
1513- 'POST'
1514- )
1497+ request = make_jwt_request ("invalid-jwt" , grant_type = "password" )
15151498 request .content_type = 'application/x-www-form-urlencoded'
15161499
15171500 response = self .xblock .lti_1p3_access_token (request )
@@ -1526,17 +1509,7 @@ def test_access_token_invalid_client(self):
15261509 self .xblock .save ()
15271510
15281511 jwt = create_jwt (self .key , {})
1529- request = make_request (
1530- urllib .parse .urlencode ({
1531- "grant_type" : "client_credentials" ,
1532- "client_assertion_type" : "something" ,
1533- "client_assertion" : jwt ,
1534- "scope" : "" ,
1535- }),
1536- 'POST'
1537- )
1538- request .content_type = 'application/x-www-form-urlencoded'
1539-
1512+ request = make_jwt_request (jwt )
15401513 response = self .xblock .lti_1p3_access_token (request )
15411514 self .assertEqual (response .status_code , 400 )
15421515 self .assertEqual (response .json_body , {'error' : 'invalid_client' })
@@ -1546,17 +1519,7 @@ def test_access_token(self):
15461519 Test request with valid JWT.
15471520 """
15481521 jwt = create_jwt (self .key , {})
1549- request = make_request (
1550- urllib .parse .urlencode ({
1551- "grant_type" : "client_credentials" ,
1552- "client_assertion_type" : "something" ,
1553- "client_assertion" : jwt ,
1554- "scope" : "" ,
1555- }),
1556- 'POST'
1557- )
1558- request .content_type = 'application/x-www-form-urlencoded'
1559-
1522+ request = make_jwt_request (jwt )
15601523 response = self .xblock .lti_1p3_access_token (request )
15611524 self .assertEqual (response .status_code , 200 )
15621525
@@ -1633,3 +1596,87 @@ def test_lti_custom_param_templates_not_configured(self, mock_import_module, moc
16331596 self .assertEqual (resolved_value , custom_parameter_template_value )
16341597 assert mock_log .error .called
16351598 mock_import_module .asser_not_called ()
1599+
1600+
1601+ class TestLti1p3AccessTokenJWK (TestCase ):
1602+ """
1603+ Unit tests for LtiConsumerXBlock Access Token endpoint when using a
1604+ LTI 1.3 setup with JWK authentication.
1605+ """
1606+ def setUp (self ):
1607+ super ().setUp ()
1608+ self .xblock = make_xblock ('lti_consumer' , LtiConsumerXBlock , {
1609+ 'lti_version' : 'lti_1p3' ,
1610+ 'lti_1p3_launch_url' : 'http://tool.example/launch' ,
1611+ 'lti_1p3_oidc_url' : 'http://tool.example/oidc' ,
1612+ 'lti_1p3_tool_keyset_url' : "http://tool.example/keyset" ,
1613+ })
1614+ self .xblock .location = 'block-v1:course+test+2020+type@problem+block@test'
1615+ self .xblock .save ()
1616+
1617+ self .key = RSAKey (key = RSA .generate (2048 ), kid = "1" )
1618+
1619+ jwt = create_jwt (self .key , {})
1620+ self .request = make_jwt_request (jwt )
1621+
1622+ def make_keyset (self , keys ):
1623+ """
1624+ Builds a keyset object with the given keys.
1625+ """
1626+ jwks = KEYS ()
1627+ jwks ._keys = keys # pylint: disable=protected-access
1628+ return jwks
1629+
1630+ @patch ("lti_consumer.lti_1p3.key_handlers.load_jwks_from_url" )
1631+ def test_access_token_using_keyset_url (self , load_jwks_from_url ):
1632+ """
1633+ Test request using the provider's keyset URL instead of a public key.
1634+ """
1635+ load_jwks_from_url .return_value = self .make_keyset ([self .key ])
1636+ response = self .xblock .lti_1p3_access_token (self .request )
1637+ load_jwks_from_url .assert_called_once_with ("http://tool.example/keyset" )
1638+ self .assertEqual (response .status_code , 200 )
1639+
1640+ @patch ("lti_consumer.lti_1p3.key_handlers.load_jwks_from_url" )
1641+ def test_access_token_using_keyset_url_with_empty_keys (self , load_jwks_from_url ):
1642+ """
1643+ Test request where the provider's keyset URL returns an empty list of keys.
1644+ """
1645+ load_jwks_from_url .return_value = self .make_keyset ([])
1646+ response = self .xblock .lti_1p3_access_token (self .request )
1647+ self .assertEqual (response .status_code , 400 )
1648+ self .assertEqual (response .json_body , {"error" : "invalid_client" })
1649+
1650+ @patch ("lti_consumer.lti_1p3.key_handlers.load_jwks_from_url" )
1651+ def test_access_token_using_keyset_url_with_wrong_keys (self , load_jwks_from_url ):
1652+ """
1653+ Test request where the provider's keyset URL returns wrong keys.
1654+ """
1655+ key = RSAKey (key = RSA .generate (2048 ), kid = "2" )
1656+ load_jwks_from_url .return_value = self .make_keyset ([key ])
1657+ response = self .xblock .lti_1p3_access_token (self .request )
1658+ self .assertEqual (response .status_code , 400 )
1659+ self .assertEqual (response .json_body , {"error" : "invalid_client" })
1660+
1661+ @patch ("jwkest.jwk.request" )
1662+ def test_access_token_using_keyset_url_that_fails (self , request ):
1663+ """
1664+ Test request where the provider's keyset URL request fails.
1665+ """
1666+ request .side_effect = Exception ("request fails" )
1667+ response = self .xblock .lti_1p3_access_token (self .request )
1668+ self .assertEqual (response .status_code , 400 )
1669+ self .assertEqual (response .json_body , {'error' : 'invalid_client' })
1670+
1671+ @patch ("jwkest.jwk.request" )
1672+ def test_access_token_using_keyset_url_with_invalid_contents (self , request ):
1673+ """
1674+ Test request where the provider's keyset URL doesn't return valid JSON.
1675+ """
1676+ response_mock = Mock ()
1677+ response_mock .status_code = 200
1678+ response_mock .text = b'this is not a valid json'
1679+ request .return_value = response_mock
1680+ response = self .xblock .lti_1p3_access_token (self .request )
1681+ self .assertEqual (response .status_code , 400 )
1682+ self .assertEqual (response .json_body , {'error' : 'invalid_client' })
0 commit comments