Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions duo_universal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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')
Expand Down
81 changes: 79 additions & 2 deletions tests/test_create_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down