Skip to content
Open
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
45 changes: 45 additions & 0 deletions class-duouniversal-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,35 @@ public function duoup_failmode_validate( $failmode ) {
return $failmode;
}

public function duo_settings_username_attribute() {
$attribute = \esc_attr( $this->duo_utils->duo_get_option( 'duoup_username_attribute', 'username' ) );
$result = '';
$result .= '<select id="duoup_username_attribute" name="duoup_username_attribute">';
$result .= sprintf(
'<option value="username" %s>%s</option>',
'username' === $attribute ? 'selected' : '',
\esc_html__( 'Username', 'duo-universal' )
);
$result .= sprintf(
'<option value="email" %s>%s</option>',
'email' === $attribute ? 'selected' : '',
\esc_html__( 'Email Address', 'duo-universal' )
);
$result .= '</select>';
$result .= '<p class="description">' . \esc_html__( 'Choose which WordPress user attribute is sent to Duo as the username. This must match how users are enrolled in Duo.', 'duo-universal' ) . '</p>';
return $result;
}

public function duoup_username_attribute_validate( $attribute ) {
$attribute = sanitize_text_field( $attribute );
if ( ! in_array( $attribute, array( 'username', 'email' ), true ) ) {
\add_settings_error( 'duoup_username_attribute', '', __( 'Username attribute value is not valid', 'duo-universal' ) );
$current_attribute = $this->duo_utils->duo_get_option( 'duoup_username_attribute', 'username' );
return $current_attribute;
}
return $attribute;
}

public function duo_settings_roles() {
$wp_roles = $this->duo_utils->duo_get_roles();
$roles = $wp_roles->get_names();
Expand Down Expand Up @@ -285,6 +314,9 @@ public function printing_callback( $text ) {
'selected' => array(),
),
'br' => array(),
'p' => array(
'class' => array(),
),
),
)
);
Expand All @@ -303,6 +335,7 @@ public function duo_admin_init() {
$this->duo_add_site_option( 'duoup_client_secret', '' );
$this->duo_add_site_option( 'duoup_api_host', '' );
$this->duo_add_site_option( 'duoup_failmode', '' );
$this->duo_add_site_option( 'duoup_username_attribute', 'username' );
$this->duo_add_site_option( 'duoup_roles', $allroles );
$this->duo_add_site_option( 'duoup_xmlrpc', 'off' );
} else {
Expand All @@ -311,6 +344,7 @@ public function duo_admin_init() {
$this->duoup_add_settings_field( 'duoup_client_secret', __( 'Client Secret', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_client_secret_validate' ), $this->duo_settings_client_secret() );
$this->duoup_add_settings_field( 'duoup_api_host', __( 'API hostname', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_api_host_validate' ), $this->duo_settings_host() );
$this->duoup_add_settings_field( 'duoup_failmode', __( 'Failmode', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_failmode_validate' ), $this->duo_settings_failmode() );
$this->duoup_add_settings_field( 'duoup_username_attribute', __( 'Duo Username', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_username_attribute_validate' ), $this->duo_settings_username_attribute() );
$this->duoup_add_settings_field( 'duoup_roles', __( 'Enable for roles:', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_roles_validate' ), $this->duo_settings_roles() );
$this->duoup_add_settings_field( 'duoup_xmlrpc', __( 'Disable XML-RPC (recommended)', 'duo-universal' ), array( $this, 'printing_callback' ), array( $this, 'duoup_xmlrpc_validate' ), $this->duo_settings_xmlrpc() );
}
Expand Down Expand Up @@ -342,6 +376,9 @@ public function print_field( $id, $label, $input ) {
'selected' => array(),
),
'br' => array(),
'p' => array(
'class' => array(),
),
),
)
);
Expand All @@ -358,6 +395,7 @@ public function duo_mu_options() {
$this->print_field( 'duoup_client_secret', \__( 'Client Secret', 'duo-universal' ), $this->duo_settings_client_secret() );
$this->print_field( 'duoup_api_host', \__( 'API hostname', 'duo-universal' ), $this->duo_settings_host() );
$this->print_field( 'duoup_failmode', \__( 'Failmode', 'duo-universal' ), $this->duo_settings_failmode() );
$this->print_field( 'duoup_username_attribute', \__( 'Duo Username', 'duo-universal' ), $this->duo_settings_username_attribute() );
$this->print_field( 'duoup_roles', \__( 'Roles', 'duo-universal' ), $this->duo_settings_roles() );
$this->print_field( 'duoup_xmlrpc', \__( 'Disable XML-RPC (recommended)', 'duo-universal' ), $this->duo_settings_xmlrpc() );
echo( "</table>\n" );
Expand Down Expand Up @@ -388,6 +426,13 @@ public function duo_update_mu_options() {
$result = \update_site_option( 'duoup_failmode', 'open' );
}

if ( isset( $_POST['duoup_username_attribute'] ) ) {
$attribute = $this->duoup_username_attribute_validate( sanitize_text_field( \wp_unslash( $_POST['duoup_username_attribute'] ) ) );
$result = \update_site_option( 'duoup_username_attribute', $attribute );
} else {
$result = \update_site_option( 'duoup_username_attribute', 'username' );
}

if ( isset( $_POST['duoup_roles'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$roles = $this->duoup_roles_validate( \wp_unslash( $_POST['duoup_roles'] ) );
Expand Down
23 changes: 21 additions & 2 deletions class-duouniversal-wordpressplugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ public function __construct(
$this->duo_client = $duo_client;
$this->duo_utils = $duo_utils;
}

/**
* Returns the identifier to send to Duo for a given user,
* based on the duoup_username_attribute setting.
*
* @param \WP_User $user The WordPress user object.
* @return string The username or email to pass to Duo.
*/
function get_duo_username( $user ) {
$attribute = $this->duo_utils->duo_get_option( 'duoup_username_attribute', 'username' );
if ( 'email' === $attribute ) {
return $user->user_email;
}
return $user->user_login;
}

/**
* Sets a user's auth state
* user: username of the user to update
Expand Down Expand Up @@ -132,7 +148,9 @@ function duo_start_second_factor( $user ) {

$this->update_user_auth_status( $user->ID, 'in-progress', $redirect_url, $oidc_state );

$prompt_uri = $this->duo_client->createAuthUrl( $user->user_login, $oidc_state );
$duo_username = $this->get_duo_username( $user );
$this->duo_debug_log( "Starting Duo auth for user identifier: $duo_username" );
$prompt_uri = $this->duo_client->createAuthUrl( $duo_username, $oidc_state );
\wp_redirect( $prompt_uri );
$this->exit();
}
Expand Down Expand Up @@ -196,7 +214,8 @@ function duo_authenticate_user( $user = '', $username = '', $password = '' ) {
try {
// Update redirect URL to be one associated with initial authentication.
$this->duo_client->redirect_url = $this->get_redirect_url( $associated_user->ID );
$decoded_token = $this->duo_client->exchangeAuthorizationCodeFor2FAResult( $code, $associated_user->user_login );
$duo_username = $this->get_duo_username( $associated_user );
$decoded_token = $this->duo_client->exchangeAuthorizationCodeFor2FAResult( $code, $duo_username );
} catch ( \Duo\DuoUniversal\DuoException $e ) {
$this->duo_debug_log( $e->getMessage() );
$error = $this->duo_utils->new_WP_Error(
Expand Down
127 changes: 127 additions & 0 deletions tests/duoUniversalAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function createMockUser() {
->setMockClassName('WP_User')
->getMock();
$user->user_login = "test user";
$user->user_email = "testuser@example.com";
$user->ID = 1;
return $user;
}
Expand All @@ -86,6 +87,132 @@ function setUp(): void
$this->duo_utils = $this->createMock(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class);
}

/**
* Test that get_duo_username returns user_login by default
*/
function testGetDuoUsernameDefaultIsLogin(): void
{
$duo_utils = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class)
->onlyMethods(['duo_get_option'])
->getMock();
$duo_utils->method('duo_get_option')->with('duoup_username_attribute', 'username')->willReturn('username');
$authentication = new Duo\DuoUniversalWordpress\DuoUniversal_WordPressPlugin($duo_utils, $this->duo_client);
$user = $this->createMockUser();

$result = $authentication->get_duo_username($user);

$this->assertEquals("test user", $result);
}

/**
* Test that get_duo_username returns user_email when setting is 'email'
*/
function testGetDuoUsernameReturnsEmailWhenConfigured(): void
{
$duo_utils = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class)
->onlyMethods(['duo_get_option'])
->getMock();
$duo_utils->method('duo_get_option')->with('duoup_username_attribute', 'username')->willReturn('email');
$authentication = new Duo\DuoUniversalWordpress\DuoUniversal_WordPressPlugin($duo_utils, $this->duo_client);
$user = $this->createMockUser();

$result = $authentication->get_duo_username($user);

$this->assertEquals("testuser@example.com", $result);
}

/**
* Test that createAuthUrl receives the email when username attribute is 'email'
*/
function testStartSecondFactorPassesEmailToDuo(): void
{
$this->setUpMocks();
$user = $this->createMockUser();
$duo_utils = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class)
->onlyMethods(['duo_get_option', 'duo_debug_log'])
->getMock();
$duo_utils->method('duo_get_option')->willReturn('email');
$this->duo_client->expects($this->once())
->method('createAuthUrl')
->with($this->equalTo('testuser@example.com'), $this->anything())
->willReturn('prompt url');

$authentication = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_WordPressPlugin::class)
->setConstructorArgs(array($duo_utils, $this->duo_client))
->onlyMethods(['get_page_url', 'exit'])
->getMock();
$authentication->method('get_page_url')->willReturn('fake url');
WP_Mock::passthruFunction('wp_redirect');

$authentication->duo_start_second_factor($user);
$this->assertConditionsMet();
}

/**
* Test that createAuthUrl receives user_login when username attribute is 'username'
*/
function testStartSecondFactorPassesUsernameToDuo(): void
{
$this->setUpMocks();
$user = $this->createMockUser();
$duo_utils = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class)
->onlyMethods(['duo_get_option', 'duo_debug_log'])
->getMock();
$duo_utils->method('duo_get_option')->willReturn('username');
$this->duo_client->expects($this->once())
->method('createAuthUrl')
->with($this->equalTo('test user'), $this->anything())
->willReturn('prompt url');

$authentication = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_WordPressPlugin::class)
->setConstructorArgs(array($duo_utils, $this->duo_client))
->onlyMethods(['get_page_url', 'exit'])
->getMock();
$authentication->method('get_page_url')->willReturn('fake url');
WP_Mock::passthruFunction('wp_redirect');

$authentication->duo_start_second_factor($user);
$this->assertConditionsMet();
}

/**
* Test that exchangeAuthorizationCodeFor2FAResult receives the email
* when username attribute is 'email' during secondary auth
*/
function testSecondaryAuthPassesEmailToDuo(): void
{
$this->setUpMocks();
WP_Mock::passthruFunction('wp_unslash');
$user = $this->createMockUser();
$duo_utils = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_Utilities::class)
->onlyMethods(['duo_get_option', 'duo_debug_log', 'duo_auth_enabled', 'new_WP_user'])
->getMock();
$duo_utils->method('duo_auth_enabled')->willReturn(true);
$duo_utils->method('duo_get_option')->willReturn('email');
$duo_utils->method('new_WP_user')->willReturnArgument(1);

$this->duo_client->expects($this->once())
->method('exchangeAuthorizationCodeFor2FAResult')
->with($this->equalTo('testcode'), $this->equalTo('testuser@example.com'));

$authentication = $this->getMockBuilder(Duo\DuoUniversalWordpress\DuoUniversal_WordPressPlugin::class)
->setConstructorArgs(array($duo_utils, $this->duo_client))
->onlyMethods(
[
'get_user_from_oidc_state',
'get_redirect_url'
]
)
->getMock();
$authentication->method('get_user_from_oidc_state')->willReturn($user);

$_GET['duo_code'] = "testcode";
$_GET['state'] = "teststate";

$authentication->duo_authenticate_user();
$this->assertConditionsMet();
}

/**
* Test that update_user_auth_status creates
* correct user metadata
Expand Down
Loading