diff --git a/duo_universal/client.py b/duo_universal/client.py index a44ef33..95522cd 100644 --- a/duo_universal/client.py +++ b/duo_universal/client.py @@ -240,16 +240,23 @@ def health_check(self): return res - def create_auth_url(self, username, state, nonce=None): + def create_auth_url(self, username, state, nonce=None, dest_app_name=None, + dest_app_id=None, display_username=None): """Generate uri to Duo's prompt Arguments: - username -- username trying to authenticate with Duo - state -- Randomly generated character string of at least 16 - and at most 1024 characters returned to the integration by Duo after 2FA - nonce -- Randomly generated character string of at least 16 - and at most 1024 characters used as the nonce for the underlying OIDC flow + username -- username trying to authenticate with Duo + state -- Randomly generated character string of at least 16 + and at most 1024 characters returned to the integration by Duo after 2FA + nonce -- Randomly generated character string of at least 16 + and at most 1024 characters used as the nonce for the underlying OIDC flow + dest_app_name -- (Optional) User-facing name of the application the user is + authenticating to, shown in Duo Mobile and the auth log + dest_app_id -- (Optional) Long-lived unique identifier for the destination + application; not shown to users + display_username -- (Optional) Username shown in the Duo Mobile "user" field for Push. + Defaults to the Duo username if not provided. Returns: @@ -273,6 +280,13 @@ def create_auth_url(self, username, state, nonce=None): 'use_duo_code_attribute': self._use_duo_code_attribute, } + if dest_app_name is not None: + jwt_args['dest_app_name'] = dest_app_name + if dest_app_id is not None: + jwt_args['dest_app_id'] = dest_app_id + if display_username is not None: + jwt_args['display_username'] = display_username + request_jwt = jwt.encode(jwt_args, self._signing_key, algorithm='HS512') diff --git a/tests/test_create_auth.py b/tests/test_create_auth.py index 1887620..34c1f0a 100644 --- a/tests/test_create_auth.py +++ b/tests/test_create_auth.py @@ -10,6 +10,9 @@ REDIRECT_URI = "https://www.example.com" USERNAME = "user1" STATE = "deadbeefdeadbeefdeadbeefdeadbeefdead" +DEST_APP_NAME = "Example App" +DEST_APP_ID = "example-app-1234" +DISPLAY_USERNAME = "User One" NONE = None @@ -60,7 +63,81 @@ def test_use_duo_code_false(self): self._assert_client_creates_expected_uri(duo_client, expected_jwt_args) - def _assert_client_creates_expected_uri(self, duo_client, expected_jwt_args): + @patch('time.time', MagicMock(return_value=2)) + def test_dest_app_name_and_dest_app_id(self): + """ + Test create_auth_url includes dest_app_name and dest_app_id when given + """ + duo_client = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI) + + expected_jwt_args = { + 'scope': 'openid', + 'redirect_uri': REDIRECT_URI, + 'client_id': CLIENT_ID, + 'iss': CLIENT_ID, + 'aud': client.API_HOST_URI_FORMAT.format(HOST), + 'exp': 302, + 'state': STATE, + 'response_type': 'code', + 'duo_uname': USERNAME, + 'use_duo_code_attribute': True, + 'dest_app_name': DEST_APP_NAME, + 'dest_app_id': DEST_APP_ID, + } + + self._assert_client_creates_expected_uri( + duo_client, expected_jwt_args, + dest_app_name=DEST_APP_NAME, dest_app_id=DEST_APP_ID) + + @patch('time.time', MagicMock(return_value=2)) + def test_display_username(self): + """ + Test create_auth_url includes display_username when given + """ + duo_client = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI) + + expected_jwt_args = { + 'scope': 'openid', + 'redirect_uri': REDIRECT_URI, + 'client_id': CLIENT_ID, + 'iss': CLIENT_ID, + 'aud': client.API_HOST_URI_FORMAT.format(HOST), + 'exp': 302, + 'state': STATE, + 'response_type': 'code', + 'duo_uname': USERNAME, + 'use_duo_code_attribute': True, + 'display_username': DISPLAY_USERNAME, + } + + self._assert_client_creates_expected_uri( + duo_client, expected_jwt_args, display_username=DISPLAY_USERNAME) + + @patch('time.time', MagicMock(return_value=2)) + def test_dest_app_name_without_dest_app_id(self): + """ + Test create_auth_url includes dest_app_name on its own + """ + duo_client = client.Client(CLIENT_ID, CLIENT_SECRET, HOST, REDIRECT_URI) + + expected_jwt_args = { + 'scope': 'openid', + 'redirect_uri': REDIRECT_URI, + 'client_id': CLIENT_ID, + 'iss': CLIENT_ID, + 'aud': client.API_HOST_URI_FORMAT.format(HOST), + 'exp': 302, + 'state': STATE, + 'response_type': 'code', + 'duo_uname': USERNAME, + 'use_duo_code_attribute': True, + 'dest_app_name': DEST_APP_NAME, + } + + self._assert_client_creates_expected_uri( + duo_client, expected_jwt_args, dest_app_name=DEST_APP_NAME) + + def _assert_client_creates_expected_uri(self, duo_client, expected_jwt_args, **kwargs): authorize_endpoint = \ client.OAUTH_V1_AUTHORIZE_ENDPOINT.format(HOST) @@ -74,7 +151,7 @@ def _assert_client_creates_expected_uri(self, duo_client, expected_jwt_args): expected_authorization_uri = "{}?{}".format(authorize_endpoint, encoded_all_args) - actual_authorization_uri = duo_client.create_auth_url(USERNAME, STATE) + actual_authorization_uri = duo_client.create_auth_url(USERNAME, STATE, **kwargs) self.assertEqual(expected_authorization_uri, actual_authorization_uri)