PluginProbe
Authorizer / 3.9.1
Authorizer v3.9.1
3.16.0 3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 All 127 releases
← All changes | src/authorizer/class-authentication.php +732 -89 2.9.13 → 3.9.1 View file →
@@ -16,12 +16,13 @@
16 16 /**
17 17 * Implements the authentication (is user who they say they are?) features of
18 18 * the plugin.
19 19 */
20 -class Authentication extends Static_Instance {
20 +class Authentication extends Singleton {
21 21
22 22 /**
23 23 * Tracks the external service used by the user currently logging out.
24 + *
24 25 * @var string
25 26 */
26 27 private static $authenticated_by = '';
27 28
@@ -75,19 +76,19 @@
75 76 $auth_settings = $options->get_all( Helper::SINGLE_CONTEXT, 'allow override' );
76 77
77 78 // Make sure $last_attempt (time) and $num_attempts are positive integers.
78 79 // Note: this addresses resetting them if either is unset from above.
79 - $last_attempt = abs( intval( $last_attempt ) );
80 - $num_attempts = abs( intval( $num_attempts ) );
80 + $last_attempt = absint( $last_attempt );
81 + $num_attempts = absint( $num_attempts );
81 82
82 83 // Create semantic lockout variables.
83 84 $lockouts = $auth_settings['advanced_lockouts'];
84 85 $time_since_last_fail = time() - $last_attempt;
85 - $reset_duration = $lockouts['reset_duration'] * 60; // minutes to seconds.
86 - $num_attempts_long_lockout = $lockouts['attempts_1'] + $lockouts['attempts_2'];
87 - $num_attempts_short_lockout = $lockouts['attempts_1'];
88 - $seconds_remaining_long_lockout = $lockouts['duration_2'] * 60 - $time_since_last_fail;
89 - $seconds_remaining_short_lockout = $lockouts['duration_1'] * 60 - $time_since_last_fail;
86 + $reset_duration = absint( $lockouts['reset_duration'] ) * 60; // minutes to seconds.
87 + $num_attempts_long_lockout = absint( $lockouts['attempts_1'] ) + absint( $lockouts['attempts_2'] );
88 + $num_attempts_short_lockout = absint( $lockouts['attempts_1'] );
89 + $seconds_remaining_long_lockout = absint( $lockouts['duration_2'] ) * 60 - $time_since_last_fail;
90 + $seconds_remaining_short_lockout = absint( $lockouts['duration_1'] ) * 60 - $time_since_last_fail;
90 91
91 92 // Check if we need to institute a lockout delay.
92 93 if ( $is_login_attempt && $time_since_last_fail > $reset_duration ) {
93 94 // Enough time has passed since the last invalid attempt and
@@ -136,8 +137,26 @@
136 137 $externally_authenticated_emails = array();
137 138 $authenticated_by = '';
138 139 $result = null;
139 140
141 + // Try OAuth2 authentication if it's enabled and we don't have a
142 + // successful login yet.
143 + if (
144 + '1' === $auth_settings['oauth2'] &&
145 + 0 === count( $externally_authenticated_emails ) &&
146 + ! is_wp_error( $result )
147 + ) {
148 + $result = $this->custom_authenticate_oauth2( $auth_settings );
149 + if ( ! is_null( $result ) && ! is_wp_error( $result ) ) {
150 + if ( is_array( $result['email'] ) ) {
151 + $externally_authenticated_emails = $result['email'];
152 + } else {
153 + $externally_authenticated_emails[] = $result['email'];
154 + }
155 + $authenticated_by = $result['authenticated_by'];
156 + }
157 + }
158 +
140 159 // Try Google authentication if it's enabled and we don't have a
141 160 // successful login yet.
142 161 if (
143 162 '1' === $auth_settings['google'] &&
@@ -200,8 +219,9 @@
200 219 array_key_exists( 'advanced_disable_wp_login', $auth_settings ) &&
201 220 '1' === $auth_settings['advanced_disable_wp_login'] &&
202 221 (
203 222 '1' === $auth_settings['cas'] ||
223 + '1' === $auth_settings['oauth2'] ||
204 224 '1' === $auth_settings['google'] ||
205 225 '1' === $auth_settings['ldap']
206 226 )
207 227 ) {
@@ -279,8 +299,388 @@
279 299 }
280 300
281 301
282 302 /**
303 + * Validate this user's credentials against selected OAuth2 provider.
304 + *
305 + * @param array $auth_settings Plugin settings.
306 + * @return array|WP_Error Array containing email, authenticated_by, first_name,
307 + * last_name, and username strings for the successfully
308 + * authenticated user, or WP_Error() object on failure,
309 + * or null if not attempting an oauth2 login.
310 + */
311 + protected function custom_authenticate_oauth2( $auth_settings ) {
312 + // Move on if oauth2 hasn't been requested here.
313 + // phpcs:ignore WordPress.Security.NonceVerification
314 + if ( empty( $_GET['external'] ) || 'oauth2' !== $_GET['external'] ) {
315 + // Note: because Azure oauth2 provider doesn't let us specify a querystring
316 + // in the redirect_uri, we have to detect those redirects separately because
317 + // we can't include external=oauth2 in the redirect_uri. Instead, detect the
318 + // absence of the `external` param, and the presence of `code` and `state`
319 + // params.
320 + if ( ! empty( $_GET['external'] ) || ( empty( $_GET['code'] ) && empty( $_GET['state'] ) ) ) {
321 + return null;
322 + }
323 + }
324 +
325 + // Fetch the Oauth2 Client ID (allow overrides from filter or constant).
326 + if ( defined( 'AUTHORIZER_OAUTH2_CLIENT_ID' ) ) {
327 + $auth_settings['oauth2_clientid'] = \AUTHORIZER_OAUTH2_CLIENT_ID;
328 + }
329 + /**
330 + * Filters the Oauth2 Client ID used by Authorizer to authenticate.
331 + *
332 + * @since 3.9.0
333 + *
334 + * @param string $oauth2_client_id The stored Oauth2 Client ID.
335 + */
336 + $auth_settings['oauth2_clientid'] = apply_filters( 'authorizer_oauth2_client_id', $auth_settings['oauth2_clientid'] );
337 +
338 + // Fetch the Oauth2 Client Secret (allow overrides from filter or constant).
339 + if ( defined( 'AUTHORIZER_OAUTH2_CLIENT_SECRET' ) ) {
340 + $auth_settings['oauth2_clientsecret'] = \AUTHORIZER_OAUTH2_CLIENT_SECRET;
341 + }
342 + /**
343 + * Filters the Oauth2 Client Secret used by Authorizer to authenticate.
344 + *
345 + * @since 3.6.1
346 + *
347 + * @param string $oauth2_client_secret The stored Oauth2 Client Secret.
348 + */
349 + $auth_settings['oauth2_clientsecret'] = apply_filters( 'authorizer_oauth2_client_secret', $auth_settings['oauth2_clientsecret'] );
350 +
351 + // Move on if required params aren't specified in settings.
352 + if (
353 + empty( $auth_settings['oauth2_clientid'] ) ||
354 + empty( $auth_settings['oauth2_clientsecret'] )
355 + ) {
356 + return null;
357 + }
358 +
359 + // Authenticate with GitHub.
360 + // See: https://github.com/thephpleague/oauth2-github.
361 + if ( 'github' === $auth_settings['oauth2_provider'] ) {
362 + session_start();
363 + $provider = new \League\OAuth2\Client\Provider\Github( array(
364 + 'clientId' => $auth_settings['oauth2_clientid'],
365 + 'clientSecret' => $auth_settings['oauth2_clientsecret'],
366 + 'redirectUri' => site_url( '/wp-login.php?external=oauth2' ),
367 + ) );
368 +
369 + // If we don't have an authorization code, then get one.
370 + if ( ! isset( $_REQUEST['code'] ) ) {
371 + $auth_url = $provider->getAuthorizationUrl( array(
372 + 'scope' => 'user:email',
373 + ) );
374 + $_SESSION['oauth2state'] = $provider->getState();
375 + header( 'Location: ' . $auth_url );
376 + exit;
377 +
378 + } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
379 + // Check state against previously stored one to mitigate CSRF attacks.
380 + unset( $_SESSION['oauth2state'] );
381 + exit;
382 +
383 + } else {
384 + // Try to get an access token (using the authorization code grant).
385 + try {
386 + $token = $provider->getAccessToken( 'authorization_code', array(
387 + 'code' => $_REQUEST['code'],
388 + ) );
389 + } catch ( \Exception $e ) {
390 + // Failed to get token; try again from the beginning. Usually a
391 + // bad_verification_code error. See: https://docs.github.com/en/free-pro-team@latest/developers/apps/troubleshooting-oauth-app-access-token-request-errors#bad-verification-code.
392 + $auth_url = $provider->getAuthorizationUrl( array(
393 + 'scope' => 'user:email',
394 + ) );
395 + $_SESSION['oauth2state'] = $provider->getState();
396 + header( 'Location: ' . $auth_url );
397 + exit;
398 + }
399 +
400 + try {
401 + // Look up user using token.
402 + $user = $provider->getResourceOwner( $token );
403 +
404 + $email = $user->getEmail();
405 + $username = $user->getNickname();
406 + $attributes = $user->toArray();
407 +
408 + // If user has no public email, fetch all emails and use those.
409 + if ( empty( $email ) ) {
410 + $request = $provider->getAuthenticatedRequest(
411 + 'GET',
412 + $provider->getResourceOwnerDetailsUrl( $token ) . '/emails',
413 + $token
414 + );
415 + $attributes['emails'] = array_filter( array_map(
416 + function ( $entry ) {
417 + return empty( $entry['email'] ) ? '' : $entry['email'];
418 + },
419 + (array) $provider->getParsedResponse( $request )
420 + ) );
421 + $email = $attributes['emails'];
422 + }
423 + } catch ( \Exception $e ) {
424 + // Failed to get user details.
425 + return null;
426 + }
427 + }
428 + } elseif ( 'azure' === $auth_settings['oauth2_provider'] ) {
429 + // Authenticate with the Microsoft Azure oauth2 client.
430 + // See: https://github.com/thenetworg/oauth2-azure.
431 + session_start();
432 + try {
433 + // Save the redirect URL for WordPress so we can restore it after a
434 + // successful login (note: we can't add the redirect_to querystring
435 + // param to the redirectUri param below because it won't match the
436 + // approved URI set in the Azure portal).
437 + $login_querystring = array();
438 + if ( isset( $_SERVER['QUERY_STRING'] ) ) {
439 + parse_str( $_SERVER['QUERY_STRING'], $login_querystring ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
440 + }
441 + if ( isset( $login_querystring['redirect_to'] ) ) {
442 + $_SESSION['azure_redirect_to'] = $login_querystring['redirect_to'];
443 + }
444 +
445 + $provider = new \TheNetworg\OAuth2\Client\Provider\Azure( array(
446 + 'clientId' => $auth_settings['oauth2_clientid'],
447 + 'clientSecret' => $auth_settings['oauth2_clientsecret'],
448 + 'redirectUri' => site_url( '/wp-login.php' ),
449 + 'tenant' => empty( $auth_settings['oauth2_tenant_id'] ) ? 'common' : $auth_settings['oauth2_tenant_id'],
450 + ) );
451 + // Use v2 API. Set to Azure::ENDPOINT_VERSION_1_0 to use v1 API.
452 + $provider->defaultEndPointVersion = \TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;
453 +
454 + $baseGraphUri = $provider->getRootMicrosoftGraphUri( null );
455 + $provider->scope = 'openid profile email offline_access ' . $baseGraphUri . '/User.Read';
456 + } catch ( \Exception $e ) {
457 + // Invalid configuration, so this in not a successful login. Show error
458 + // message to user.
459 + return new \WP_Error( 'empty_username', $e->getMessage() );
460 + }
461 +
462 + // If we don't have an authorization code, then get one.
463 + if ( ! isset( $_REQUEST['code'] ) ) {
464 + try {
465 + $auth_url = $provider->getAuthorizationUrl( array(
466 + 'scope' => $provider->scope,
467 + ) );
468 + $_SESSION['oauth2state'] = $provider->getState();
469 + header( 'Location: ' . $auth_url );
470 + exit;
471 + } catch ( \Exception $e ) {
472 + // Invalid configuration, so this in not a successful login. Show error
473 + // message to user.
474 + return new \WP_Error( 'empty_username', $e->getMessage() );
475 + }
476 + } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
477 + // Check state against previously stored one to mitigate CSRF attacks.
478 + unset( $_SESSION['oauth2state'] );
479 + exit;
480 + } else {
481 + // Try to get an access token (using the authorization code grant).
482 + try {
483 + $token = $provider->getAccessToken( 'authorization_code', array(
484 + 'code' => $_REQUEST['code'],
485 + 'scope' => $provider->scope,
486 + ) );
487 + } catch ( \Exception $e ) {
488 + // Failed to get token; try again from the beginning.
489 + $auth_url = $provider->getAuthorizationUrl( array(
490 + 'scope' => $provider->scope,
491 + ) );
492 + $_SESSION['oauth2state'] = $provider->getState();
493 + header( 'Location: ' . $auth_url );
494 + exit;
495 + }
496 +
497 + try {
498 + // Look up user using token.
499 + $user = $provider->getResourceOwner( $token );
500 +
501 + $attributes = $user->toArray();
502 + $email = empty( $attributes['email'] ) ? '' : $attributes['email'];
503 + $username = empty( $attributes['preferred_username'] ) ? '' : $attributes['preferred_username'];
504 +
505 + // Attempt to find an email address in the resource owner attributes
506 + // if we couldn't find one in the `email` attribute.
507 + if ( empty( $email ) ) {
508 + $email = Helper::find_emails_in_multi_array( $attributes );
509 + }
510 + } catch ( \Exception $e ) {
511 + // Failed to get user details.
512 + return null;
513 + }
514 +
515 + /**
516 + * Filter the generic oauth2 authenticated user email.
517 + *
518 + * @param string $email Discovered email (or empty string).
519 + *
520 + * @param array $attributes Resource Owner attributes returned from oauth2 endpoint.
521 + */
522 + $email = apply_filters( 'authorizer_oauth2_generic_authenticated_email', $email, $attributes );
523 +
524 + // Set the username to the email prefix (if we don't have one).
525 + if ( ! empty( $email ) && empty( $username ) ) {
526 + if ( is_array( $email ) && ! empty( $email[0] ) ) {
527 + $username = current( explode( '@', $email[0] ) );
528 + } else {
529 + $username = current( explode( '@', $email ) );
530 + }
531 + }
532 + }
533 + } elseif ( 'generic' === $auth_settings['oauth2_provider'] ) {
534 + // Authenticate with the generic oauth2 client.
535 + // See: https://github.com/thephpleague/oauth2-client.
536 + // Move on if required params aren't specified in settings.
537 + if (
538 + empty( $auth_settings['oauth2_url_authorize'] ) ||
539 + empty( $auth_settings['oauth2_url_token'] ) ||
540 + empty( $auth_settings['oauth2_url_resource'] )
541 + ) {
542 + return null;
543 + }
544 +
545 + session_start();
546 + $provider = new \League\OAuth2\Client\Provider\GenericProvider( array(
547 + 'clientId' => $auth_settings['oauth2_clientid'],
548 + 'clientSecret' => $auth_settings['oauth2_clientsecret'],
549 + 'redirectUri' => site_url( '/wp-login.php?external=oauth2' ),
550 + 'urlAuthorize' => $auth_settings['oauth2_url_authorize'],
551 + 'urlAccessToken' => $auth_settings['oauth2_url_token'],
552 + 'urlResourceOwnerDetails' => $auth_settings['oauth2_url_resource'],
553 + ) );
554 +
555 + // If we don't have an authorization code, then get one.
556 + if ( ! isset( $_REQUEST['code'] ) ) {
557 + $auth_url = $provider->getAuthorizationUrl(
558 + /**
559 + * Filter the parameters passed to the generic oauth2 authorization endpoint.
560 + *
561 + * @param array() $params Array of key/value pairs where keys represent
562 + * a GET param and value is its value.
563 + */
564 + apply_filters( 'authorizer_oauth2_generic_authorization_parameters', array() )
565 + );
566 + $_SESSION['oauth2state'] = $provider->getState();
567 + header( 'Location: ' . $auth_url );
568 + exit;
569 + } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
570 + // Check state against previously stored one to mitigate CSRF attacks.
571 + unset( $_SESSION['oauth2state'] );
572 + exit;
573 + } else {
574 + // Try to get an access token (using the authorization code grant).
575 + try {
576 + $token = $provider->getAccessToken( 'authorization_code', array(
577 + 'code' => $_REQUEST['code'],
578 + ) );
579 + } catch ( \Exception $e ) {
580 + // Failed to get token; try again from the beginning.
581 + $auth_url = $provider->getAuthorizationUrl(
582 + /**
583 + * Filter the parameters passed to the generic oauth2 authorization endpoint.
584 + *
585 + * @param array() $params Array of key/value pairs where keys represent
586 + * a GET param and value is its value.
587 + */
588 + apply_filters( 'authorizer_oauth2_generic_authorization_parameters', array() )
589 + );
590 + $_SESSION['oauth2state'] = $provider->getState();
591 + header( 'Location: ' . $auth_url );
592 + exit;
593 + }
594 +
595 + try {
596 + // Look up user using token.
597 + $user = $provider->getResourceOwner( $token );
598 +
599 + $email = '';
600 + $username = '';
601 + $attributes = $user->toArray();
602 +
603 + // Attempt to find an email address in the resource owner attributes.
604 + $email = Helper::find_emails_in_multi_array( $attributes );
605 + } catch ( \Exception $e ) {
606 + // Failed to get user details.
607 + return null;
608 + }
609 +
610 + /**
611 + * Filter the generic oauth2 authenticated user email.
612 + *
613 + * @param string $email Discovered email (or empty string).
614 + *
615 + * @param array $attributes Resource Owner attributes returned from oauth2 endpoint.
616 + */
617 + $email = apply_filters( 'authorizer_oauth2_generic_authenticated_email', $email, $attributes );
618 +
619 + // Set the username to the email prefix (if we don't have one).
620 + if ( ! empty( $email ) && empty( $username ) ) {
621 + if ( is_array( $email ) && ! empty( $email[0] ) ) {
622 + $username = current( explode( '@', $email[0] ) );
623 + } else {
624 + $username = current( explode( '@', $email ) );
625 + }
626 + }
627 + }
628 + } else {
629 + // Move on if a supported providers wasn't selected.
630 + return null;
631 + }
632 +
633 + // Make sure email is lowercase.
634 + if ( is_array( $email ) ) {
635 + $externally_authenticated_email = array();
636 + foreach ( $email as $external_email ) {
637 + $externally_authenticated_email[] = Helper::lowercase( $external_email );
638 + }
639 + } else {
640 + $externally_authenticated_email = array_filter( array( Helper::lowercase( $email ) ) );
641 + }
642 +
643 + // Move on if no emails were found.
644 + if ( empty( $externally_authenticated_email ) ) {
645 + return null;
646 + }
647 +
648 + /**
649 + * Fail if hosteddomain param is set and the logging in user's email address
650 + * doesn't match the allowed hosted domain.
651 + */
652 + if (
653 + array_key_exists( 'oauth2_hosteddomain', $auth_settings ) &&
654 + strlen( $auth_settings['oauth2_hosteddomain'] ) > 0
655 + ) {
656 + // Allow multiple whitelisted domains.
657 + $oauth2_hosteddomains = explode( "\n", str_replace( "\r", '', $auth_settings['oauth2_hosteddomain'] ) );
658 + $valid_domain = false;
659 + foreach ( $externally_authenticated_email as $email ) {
660 + $email_domain = substr( strrchr( $email, '@' ), 1 );
661 + if ( in_array( $email_domain, $oauth2_hosteddomains, true ) ) {
662 + $valid_domain = true;
663 + }
664 + }
665 + if ( ! $valid_domain ) {
666 + $this->custom_logout();
667 + return new \WP_Error( 'invalid_oauth2_login', __( 'Email address does not match the allowed hosted domain', 'authorizer' ) );
668 + }
669 + }
670 +
671 + return array(
672 + 'email' => $externally_authenticated_email,
673 + 'username' => sanitize_user( $username ),
674 + 'first_name' => '',
675 + 'last_name' => '',
676 + 'authenticated_by' => 'oauth2',
677 + 'oauth2_provider' => $auth_settings['oauth2_provider'],
678 + 'oauth2_attributes' => $attributes,
679 + );
680 + }
681 +
682 + /**
283 683 * Validate this user's credentials against Google.
284 684 *
285 685 * @param array $auth_settings Plugin settings.
286 686 * @return array|WP_Error Array containing email, authenticated_by, first_name,
@@ -296,9 +696,9 @@
296 696 }
297 697
298 698 // Get one time use token.
299 699 session_start();
300 - $token = array_key_exists( 'token', $_SESSION ) ? json_decode( $_SESSION['token'], true ) : null;
700 + $token = array_key_exists( 'token', $_SESSION ) ? $_SESSION['token'] : null;
301 701
302 702 // No token, so this is not a succesful Google login.
303 703 if ( empty( $token ) ) {
304 704 return null;
@@ -303,19 +703,39 @@
303 703 if ( empty( $token ) ) {
304 704 return null;
305 705 }
306 706
307 - // Add Google API PHP Client.
308 - // @see https://github.com/googleapis/google-api-php-client/releases v2.2.4_PHP54
309 - if ( ! class_exists( 'Google_Client' ) ) {
310 - require_once dirname( plugin_root() ) . '/vendor/google-api-php-client-v2/vendor/autoload.php';
707 + // Fetch the Google Client ID (allow overrides from filter or constant).
708 + if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_ID' ) ) {
709 + $auth_settings['google_clientid'] = \AUTHORIZER_GOOGLE_CLIENT_ID;
311 710 }
711 + /**
712 + * Filters the Google Client ID used by Authorizer to authenticate.
713 + *
714 + * @since 3.9.0
715 + *
716 + * @param string $google_client_id The stored Google Client ID.
717 + */
718 + $auth_settings['google_clientid'] = apply_filters( 'authorizer_google_client_id', $auth_settings['google_clientid'] );
312 719
720 + // Fetch the Google Client Secret (allow overrides from filter or constant).
721 + if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_SECRET' ) ) {
722 + $auth_settings['google_clientsecret'] = \AUTHORIZER_GOOGLE_CLIENT_SECRET;
723 + }
724 + /**
725 + * Filters the Google Client Secret used by Authorizer to authenticate.
726 + *
727 + * @since 3.6.1
728 + *
729 + * @param string $google_client_secret The stored Google Client Secret.
730 + */
731 + $auth_settings['google_clientsecret'] = apply_filters( 'authorizer_google_client_secret', $auth_settings['google_clientsecret'] );
732 +
313 733 // Build the Google Client.
314 734 $client = new \Google_Client();
315 735 $client->setApplicationName( 'WordPress' );
316 - $client->setClientId( $auth_settings['google_clientid'] );
317 - $client->setClientSecret( $auth_settings['google_clientsecret'] );
736 + $client->setClientId( trim( $auth_settings['google_clientid'] ) );
737 + $client->setClientSecret( trim( $auth_settings['google_clientsecret'] ) );
318 738 $client->setRedirectUri( 'postmessage' );
319 739
320 740 /**
321 741 * If the hosted domain parameter is set, restrict logins to that domain
@@ -326,19 +746,22 @@
326 746 strlen( $auth_settings['google_hosteddomain'] ) > 0 &&
327 747 $client::LIBVER >= '2.0.0'
328 748 ) {
329 749 $google_hosteddomains = explode( "\n", str_replace( "\r", '', $auth_settings['google_hosteddomain'] ) );
330 - $google_hosteddomain = trim( $google_hosteddomains[0] );
750 + $google_hosteddomain = trim( $google_hosteddomains[0] );
331 751 $client->setHostedDomain( $google_hosteddomain );
332 752 }
333 753
754 + // Allow minor clock drift between this server's clock and Google's.
755 + // See: https://github.com/googleapis/google-api-php-client/issues/1630
756 + \Firebase\JWT\JWT::$leeway = 30;
757 +
334 758 // Verify this is a successful Google authentication.
335 - // NOTE: verifyIdToken originally returned an object as per vendor/google/auth/src/OAuth2.php.
336 - // However, it looks as though this function is overridden by src/Google/Client.php and returns an array instead
337 - // in the v2 library. Treating as an array for purposes of this functionality.
338 - // See https://github.com/googleapis/google-api-php-client/blob/master/src/Google/AccessToken/Verify.php#L77
339 759 try {
340 - $ticket = $client->verifyIdToken( $token['id_token'], $auth_settings['google_clientid'] );
760 + $payload = $client->verifyIdToken( $token );
761 + } catch ( \Firebase\JWT\BeforeValidException $e ) {
762 + // Server clock out of sync with Google servers.
763 + return new \WP_Error( 'invalid_google_login', __( 'The authentication timestamp is too old, please try again.', 'authorizer' ) );
341 764 } catch ( Google_Auth_Exception $e ) {
342 765 // Invalid ticket, so this in not a successful Google login.
343 766 return new \WP_Error( 'invalid_google_login', __( 'Invalid Google credentials provided.', 'authorizer' ) );
344 767 }
@@ -343,22 +766,14 @@
343 766 return new \WP_Error( 'invalid_google_login', __( 'Invalid Google credentials provided.', 'authorizer' ) );
344 767 }
345 768
346 769 // Invalid ticket, so this in not a successful Google login.
347 - if ( ! $ticket ) {
770 + if ( empty( $payload['email'] ) ) {
348 771 return new \WP_Error( 'invalid_google_login', __( 'Invalid Google credentials provided.', 'authorizer' ) );
349 772 }
350 773
351 774 // Get email address.
352 - // Edge case: if another plugin has already defined the Google_Client class,
353 - // and it's a version earlier than v2, then we need to handle $token as a
354 - // json-encoded string instead of an array.
355 - if ( is_object( $ticket ) && method_exists( $ticket, 'getAttributes' ) ) {
356 - $attributes = $ticket->getAttributes();
357 - $email = Helper::lowercase( $attributes['payload']['email'] );
358 - } else {
359 - $email = Helper::lowercase( $ticket['email'] );
360 - }
775 + $email = Helper::lowercase( $payload['email'] );
361 776
362 777 $email_domain = substr( strrchr( $email, '@' ), 1 );
363 778 $username = current( explode( '@', $email ) );
364 779
@@ -388,9 +803,9 @@
388 803 'username' => $username,
389 804 'first_name' => '',
390 805 'last_name' => '',
391 806 'authenticated_by' => 'google',
392 - 'google_attributes' => $ticket,
807 + 'google_attributes' => $payload,
393 808 );
394 809 }
395 810
396 811
@@ -409,23 +824,27 @@
409 824 return null;
410 825 }
411 826
412 827 /**
413 - * Get the CAS server version (default to SAML_VERSION_1_1).
828 + * Get the CAS server protocol version (default to SAML 1.1).
414 829 *
415 - * @see: https://developer.jasig.org/cas-clients/php/1.3.4/docs/api/group__public.html
830 + * @see: https://apereo.github.io/phpCAS/api/group__public.html#gadea9415f40b8d2afc39f140c9be83bbe
416 831 */
417 - $cas_version = SAML_VERSION_1_1;
418 - if ( 'CAS_VERSION_3_0' === $auth_settings['cas_version'] ) {
419 - $cas_version = CAS_VERSION_3_0;
420 - } elseif ( 'CAS_VERSION_2_0' === $auth_settings['cas_version'] ) {
421 - $cas_version = CAS_VERSION_2_0;
422 - } elseif ( 'CAS_VERSION_1_0' === $auth_settings['cas_version'] ) {
423 - $cas_version = CAS_VERSION_1_0;
424 - }
832 + $cas_version = Options\External\Cas::get_instance()->sanitize_cas_version( $auth_settings['cas_version'] );
425 833
834 + /**
835 + * Get valid service URLs for the CAS client to validate against.
836 + *
837 + * @see: https://github.com/apereo/phpCAS/security/advisories/GHSA-8q72-6qq8-xv64
838 + */
839 + $valid_base_urls = Options\External\Cas::get_instance()->get_valid_cas_service_urls();
840 +
426 841 // Set the CAS client configuration.
427 - \phpCAS::client( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'] );
842 + if ( 'PROXY' === strtoupper( $auth_settings['cas_method'] ) ) {
843 + \phpCAS::proxy( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
844 + } else {
845 + \phpCAS::client( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
846 + }
428 847
429 848 // Allow redirects at the CAS server endpoint (e.g., allow connections
430 849 // at an old CAS URL that redirects to a newer CAS URL).
431 850 \phpCAS::setExtraCurlOption( CURLOPT_FOLLOWLOCATION, true );
@@ -445,8 +864,10 @@
445 864 \phpCAS::setFixedServiceURL( $cas_service_url );
446 865
447 866 // Authenticate against CAS.
448 867 try {
868 + // phpcs:ignore Squiz.PHP.CommentedOutCode
869 + // \phpCAS::setDebug( dirname( __FILE__ ) . '/../../debug.log' );
449 870 \phpCAS::forceAuthentication();
450 871 } catch ( \CAS_AuthenticationException $e ) {
451 872 // CAS server threw an error in isAuthenticated(), potentially because
452 873 // the cached ticket is outdated. Try renewing the authentication.
@@ -507,12 +928,28 @@
507 928 }
508 929 }
509 930 }
510 931
511 - // Get user first name and last name.
512 - $first_name = array_key_exists( 'cas_attr_first_name', $auth_settings ) && strlen( $auth_settings['cas_attr_first_name'] ) > 0 && array_key_exists( $auth_settings['cas_attr_first_name'], $cas_attributes ) && strlen( $cas_attributes[ $auth_settings['cas_attr_first_name'] ] ) > 0 ? $cas_attributes[ $auth_settings['cas_attr_first_name'] ] : '';
513 - $last_name = array_key_exists( 'cas_attr_last_name', $auth_settings ) && strlen( $auth_settings['cas_attr_last_name'] ) > 0 && array_key_exists( $auth_settings['cas_attr_last_name'], $cas_attributes ) && strlen( $cas_attributes[ $auth_settings['cas_attr_last_name'] ] ) > 0 ? $cas_attributes[ $auth_settings['cas_attr_last_name'] ] : '';
932 + // Get user first name (handle string or array results from CAS attribute).
933 + $first_name = '';
934 + if ( ! empty( $auth_settings['cas_attr_first_name'] ) && ! empty( $cas_attributes[ $auth_settings['cas_attr_first_name'] ] ) ) {
935 + if ( is_string( $cas_attributes[ $auth_settings['cas_attr_first_name'] ] ) ) {
936 + $first_name = $cas_attributes[ $auth_settings['cas_attr_first_name'] ];
937 + } elseif ( is_array( $cas_attributes[ $auth_settings['cas_attr_first_name'] ] ) ) {
938 + $first_name = trim( implode( ' ', $cas_attributes[ $auth_settings['cas_attr_first_name'] ] ) );
939 + }
940 + }
514 941
942 + // Get user last name (handle string or array results from CAS attribute).
943 + $last_name = '';
944 + if ( ! empty( $auth_settings['cas_attr_last_name'] ) && ! empty( $cas_attributes[ $auth_settings['cas_attr_last_name'] ] ) ) {
945 + if ( is_string( $cas_attributes[ $auth_settings['cas_attr_last_name'] ] ) ) {
946 + $last_name = $cas_attributes[ $auth_settings['cas_attr_last_name'] ];
947 + } elseif ( is_array( $cas_attributes[ $auth_settings['cas_attr_last_name'] ] ) ) {
948 + $last_name = trim( implode( ' ', $cas_attributes[ $auth_settings['cas_attr_last_name'] ] ) );
949 + }
950 + }
951 +
515 952 return array(
516 953 'email' => $externally_authenticated_email,
517 954 'username' => $username,
518 955 'first_name' => $first_name,
@@ -528,19 +965,54 @@
528 965 *
529 966 * @param array $auth_settings Plugin settings.
530 967 * @param string $username Attempted username from authenticate action.
531 968 * @param string $password Attempted password from authenticate action.
969 + * @param array $debug If provided, filled with an array of debug
970 + * messages. Defaults to null.
971 + *
532 972 * @return array|WP_Error Array containing 'email' and 'authenticated_by' strings
533 973 * for the successfully authenticated user, or WP_Error()
534 974 * object on failure, or null if skipping LDAP auth and
535 975 * falling back to WP auth.
536 976 */
537 - protected function custom_authenticate_ldap( $auth_settings, $username, $password ) {
977 + public function custom_authenticate_ldap( $auth_settings, $username, $password, &$debug = null ) {
978 + // Make sure all LDAP settings are defined (user and password can be
979 + // overridden by constant or filter and may not exist in auth_settings).
980 + $defaults = array(
981 + 'ldap' => '',
982 + 'ldap_host' => '',
983 + 'ldap_port' => '389',
984 + 'ldap_tls' => '1',
985 + 'ldap_search_base' => '',
986 + 'ldap_search_filter' => '',
987 + 'ldap_uid' => 'uid',
988 + 'ldap_attr_email' => '',
989 + 'ldap_user' => '',
990 + 'ldap_password' => '',
991 + 'ldap_lostpassword_url' => '',
992 + 'ldap_attr_first_name' => '',
993 + 'ldap_attr_last_name' => '',
994 + 'ldap_attr_update_on_login' => '',
995 + 'ldap_test_user' => '',
996 + );
997 + $auth_settings = wp_parse_args( $auth_settings, $defaults );
998 +
999 + // Initialize debug array if a variable was passed in.
1000 + if ( ! is_null( $debug ) ) {
1001 + $debug = array(
1002 + /* TRANSLATORS: Current time */
1003 + sprintf( __( '[%s] Attempting to authenticate via LDAP.', 'authorizer' ), wp_date( get_option( 'time_format' ) ) ),
1004 + );
1005 + }
1006 +
538 1007 // Get LDAP host(s), and attempt each until we have a valid connection.
539 1008 $ldap_hosts = explode( "\n", str_replace( "\r", '', trim( $auth_settings['ldap_host'] ) ) );
540 1009
541 1010 // Fail silently (fall back to WordPress authentication) if no LDAP host specified.
542 1011 if ( count( $ldap_hosts ) < 1 ) {
1012 + if ( is_array( $debug ) ) {
1013 + $debug[] = __( 'Failed: no LDAP Host(s) specified.', 'authorizer' );
1014 + }
543 1015 return null;
544 1016 }
545 1017
546 1018 // Get LDAP search base(s).
@@ -547,8 +1019,11 @@
547 1019 $search_bases = explode( "\n", str_replace( "\r", '', trim( $auth_settings['ldap_search_base'] ) ) );
548 1020
549 1021 // Fail silently (fall back to WordPress authentication) if no search base specified.
550 1022 if ( count( $search_bases ) < 1 ) {
1023 + if ( is_array( $debug ) ) {
1024 + $debug[] = __( 'Failed: no LDAP Search Base(s) specified.', 'authorizer' );
1025 + }
551 1026 return null;
552 1027 }
553 1028
554 1029 // Get the FQDN from the first LDAP search base domain components (dc). For
@@ -577,21 +1052,33 @@
577 1052 // and password are empty (this will be the case when visiting wp-login.php
578 1053 // for the first time, or when clicking the Log In button without filling
579 1054 // out either field.
580 1055 if ( empty( $username ) && empty( $password ) ) {
1056 + if ( is_array( $debug ) ) {
1057 + $debug[] = __( 'Failed: empty username and password.', 'authorizer' );
1058 + }
581 1059 return null;
582 1060 }
583 1061
584 1062 // Fail with error message if username or password is blank.
585 1063 if ( empty( $username ) ) {
1064 + if ( is_array( $debug ) ) {
1065 + $debug[] = __( 'Failed: empty username.', 'authorizer' );
1066 + }
586 1067 return new \WP_Error( 'empty_username', __( 'You must provide a username or email.', 'authorizer' ) );
587 1068 }
588 1069 if ( empty( $password ) ) {
1070 + if ( is_array( $debug ) ) {
1071 + $debug[] = __( 'Failed: empty password.', 'authorizer' );
1072 + }
589 1073 return new \WP_Error( 'empty_password', __( 'You must provide a password.', 'authorizer' ) );
590 1074 }
591 1075
592 1076 // If php5-ldap extension isn't installed on server, fall back to WP auth.
593 1077 if ( ! function_exists( 'ldap_connect' ) ) {
1078 + if ( is_array( $debug ) ) {
1079 + $debug[] = __( 'Failed: php-ldap extension not installed.', 'authorizer' );
1080 + }
594 1081 return null;
595 1082 }
596 1083
597 1084 // Authenticate against LDAP using options provided in plugin settings.
@@ -603,18 +1090,26 @@
603 1090
604 1091 // Attempt each LDAP host until we have a valid connection.
605 1092 $ldap_valid = false;
606 1093 foreach ( $ldap_hosts as $ldap_host ) {
607 - // Construct LDAP connection parameters. ldap_connect() takes either a
608 - // hostname or a full LDAP URI as its first parameter (works with OpenLDAP
609 - // 2.x.x or later). If it's an LDAP URI, the second parameter, $port, is
610 - // ignored, and port must be specified in the full URI. An LDAP URI is of
611 - // the form ldap://hostname:port or ldaps://hostname:port.
1094 + // Construct LDAP connection parameters. In PHP < 8.3, ldap_connect()
1095 + // takes either a hostname or a full LDAP URI as its first parameter
1096 + // (works with OpenLDAP 2.x.x or later). If it's an LDAP URI, the second
1097 + // parameter, $port, is ignored, and port must be specified in the full
1098 + // URI. An LDAP URI is of the form ldap://hostname:port or
1099 + // ldaps://hostname:port.
1100 + // In PHP 8.3, ldap_connect() only takes a single param (the signature
1101 + // with 2 params is deprecated). We thus convert all LDAP hosts to a full
1102 + // LDAP URI, defaulting to ldap:// if the full URI isn't provided.
612 1103 $ldap_port = intval( $auth_settings['ldap_port'] );
613 1104 $parsed_host = wp_parse_url( $ldap_host );
614 1105
615 1106 // Fail if invalid host is specified.
616 1107 if ( false === $parsed_host ) {
1108 + if ( is_array( $debug ) ) {
1109 + /* TRANSLATORS: LDAP Host */
1110 + $debug[] = sprintf( __( 'Warning: could not parse host %s with wp_parse_url().', 'authorizer' ), $ldap_host );
1111 + }
617 1112 continue;
618 1113 }
619 1114
620 1115 // If a scheme is in the LDAP host, use full LDAP URI instead of just hostname.
@@ -623,49 +1118,144 @@
623 1118 if ( ! array_key_exists( 'port', $parsed_host ) ) {
624 1119 $parsed_host['port'] = $ldap_port;
625 1120 }
626 1121 $ldap_host = Helper::build_url( $parsed_host );
1122 + } else {
1123 + // Construct the LDAP URI from the provided host and port.
1124 + $ldap_host = 'ldap://' . $ldap_host . ':' . $ldap_port;
627 1125 }
628 1126
629 1127 // Create LDAP connection.
630 - $ldap = ldap_connect( $ldap_host, $ldap_port );
1128 + $ldap = ldap_connect( $ldap_host );
631 1129 ldap_set_option( $ldap, LDAP_OPT_PROTOCOL_VERSION, 3 );
632 1130 ldap_set_option( $ldap, LDAP_OPT_REFERRALS, 0 );
633 1131
634 1132 // Fail if we don't have a plausible LDAP URI.
635 1133 if ( false === $ldap ) {
1134 + if ( is_array( $debug ) ) {
1135 + /* TRANSLATORS: LDAP Host */
1136 + $debug[] = sprintf( __( 'Warning: syntax check failed on host %s in ldap_connect().', 'authorizer' ), $ldap_host );
1137 + }
636 1138 continue;
637 1139 }
638 1140
639 1141 // Attempt to start TLS if that setting is checked and we're not using ldaps protocol.
640 1142 if ( 1 === intval( $auth_settings['ldap_tls'] ) && false === strpos( $ldap_host, 'ldaps://' ) ) {
1143 + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
641 1144 if ( ! @ldap_start_tls( $ldap ) ) {
1145 + if ( is_array( $debug ) ) {
1146 + /* TRANSLATORS: LDAP Host */
1147 + $debug[] = sprintf( __( 'Warning: unable to start TLS on host %s:', 'authorizer' ), $ldap_host );
1148 + $debug[] = ldap_error( $ldap );
1149 + }
642 1150 continue;
643 1151 }
644 1152 }
645 1153
1154 + // Allow overrides of the LDAP user from filter or constant.
1155 + if ( defined( 'AUTHORIZER_LDAP_USER' ) ) {
1156 + $auth_settings['ldap_user'] = \AUTHORIZER_LDAP_USER;
1157 + }
1158 + /**
1159 + * Filters the LDAP user used by Authorizer to authenticate.
1160 + *
1161 + * @since 3.6.2
1162 + *
1163 + * @param string $ldap_user The stored Oauth2 Client Secret.
1164 + */
1165 + $auth_settings['ldap_user'] = apply_filters( 'authorizer_ldap_user', $auth_settings['ldap_user'] );
1166 +
1167 + // Allow overrides of the LDAP password from filter or constant.
1168 + if ( defined( 'AUTHORIZER_LDAP_PASSWORD' ) ) {
1169 + $auth_settings['ldap_password'] = \AUTHORIZER_LDAP_PASSWORD;
1170 + }
1171 + /**
1172 + * Filters the LDAP password used by Authorizer to authenticate.
1173 + *
1174 + * @since 3.6.2
1175 + *
1176 + * @param string $ldap_password The stored Oauth2 Client Secret.
1177 + */
1178 + $auth_settings['ldap_password'] = apply_filters( 'authorizer_ldap_password', $auth_settings['ldap_password'] );
1179 +
646 1180 // Set bind credentials; attempt an anonymous bind if not provided.
647 1181 $bind_rdn = null;
648 1182 $bind_password = null;
649 1183 if ( strlen( $auth_settings['ldap_user'] ) > 0 ) {
650 1184 $bind_rdn = $auth_settings['ldap_user'];
651 - $bind_password = Helper::decrypt( $auth_settings['ldap_password'] );
1185 + $bind_password = $auth_settings['ldap_password'];
1186 +
1187 + // Decrypt LDAP password if coming from wp_options database (not needed
1188 + // if it was provided via constant or filter).
1189 + if ( ! defined( 'AUTHORIZER_LDAP_PASSWORD' ) && ! has_filter( 'authorizer_ldap_password' ) ) {
1190 + $bind_password = Helper::decrypt( $bind_password );
1191 + }
1192 +
1193 + // If the bind user contains the [username] wildcard, replace it with
1194 + // the username and password of the user logging in.
1195 + if ( false !== strpos( $bind_rdn, '[username]' ) ) {
1196 + $bind_rdn = str_replace( '[username]', $username, $bind_rdn );
1197 + $bind_password = $password;
1198 +
1199 + if ( is_array( $debug ) ) {
1200 + /* TRANSLATORS: LDAP User DN */
1201 + $debug[] = sprintf( __( 'Performing bind as user logging in: %s.', 'authorizer' ), $bind_rdn );
1202 + }
1203 + }
652 1204 }
653 1205
654 1206 // Attempt LDAP bind.
655 1207 $result = @ldap_bind( $ldap, $bind_rdn, stripslashes( $bind_password ) ); // phpcs:ignore
656 1208 if ( ! $result ) {
657 - // Can't connect to LDAP, so fall back to WordPress authentication.
658 - continue;
1209 + if ( is_array( $debug ) ) {
1210 + /* TRANSLATORS: LDAP Host */
1211 + $debug[] = sprintf( __( 'Warning: unable to bind on host %1$s using directory user:', 'authorizer' ), $ldap_host );
1212 + $debug[] = ldap_error( $ldap );
1213 + }
1214 +
1215 + // We failed either an anonymous bind or a bind with a service account,
1216 + // so try to bind with the logging in user's credentials before failing.
1217 + // Note: multiple search bases can be provided, so iterate through them
1218 + // trying to bind as the user logging in.
1219 + foreach ( $search_bases as $search_base ) {
1220 + $bind_user_dn = $auth_settings['ldap_uid'] . '=' . $username . ',' . $search_base;
1221 + $result = @ldap_bind( $ldap, $bind_user_dn, stripslashes( $password ) ); // phpcs:ignore
1222 + if ( $result ) {
1223 + if ( is_array( $debug ) ) {
1224 + /* TRANSLATORS: LDAP User DN */
1225 + $debug[] = sprintf( __( 'Successful bind using LDAP user DN %s instead of directory user.', 'authorizer' ), $bind_user_dn );
1226 + }
1227 +
1228 + break;
1229 + }
1230 + }
1231 +
1232 + if ( ! $result ) {
1233 + if ( is_array( $debug ) ) {
1234 + /* TRANSLATORS: LDAP User */
1235 + $debug[] = sprintf( __( 'Failed: password incorrect for LDAP user %s.', 'authorizer' ), $username );
1236 + $debug[] = ldap_error( $ldap );
1237 + }
1238 +
1239 + // Can't connect to LDAP, so fall back to WordPress authentication.
1240 + continue;
1241 + }
659 1242 }
660 1243
661 1244 // If we've reached this, we have a valid ldap connection and bind.
662 1245 $ldap_valid = true;
1246 + if ( is_array( $debug ) ) {
1247 + /* TRANSLATORS: LDAP Host */
1248 + $debug[] = sprintf( __( 'Connected to LDAP host %s.', 'authorizer' ), $ldap_host );
1249 + }
663 1250 break;
664 1251 }
665 1252
666 1253 // Move to next authentication method if we don't have a valid LDAP connection.
667 1254 if ( ! $ldap_valid ) {
1255 + if ( is_array( $debug ) ) {
1256 + $debug[] = __( 'Failed: unable to connect to any LDAP host.', 'authorizer' );
1257 + }
668 1258 return null;
669 1259 }
670 1260
671 1261 // Look up the bind DN (and first/last name) of the user trying to
@@ -681,8 +1271,17 @@
681 1271 if ( array_key_exists( 'ldap_attr_email', $auth_settings ) && strlen( $auth_settings['ldap_attr_email'] ) > 0 && substr( $auth_settings['ldap_attr_email'], 0, 1 ) !== '@' ) {
682 1272 array_push( $ldap_attributes_to_retrieve, Helper::lowercase( $auth_settings['ldap_attr_email'] ) );
683 1273 }
684 1274
1275 + /**
1276 + * Specify additional LDAP user attributes to retrieve during authentication.
1277 + * May be used by plugins in `authorizer_user_register`.
1278 + *
1279 + * @param array $attributes LDAP attributes to retrieve in addition to first name, last name and email.
1280 + */
1281 + $additional_ldap_attributes_to_retrieve = apply_filters( 'authorizer_additional_ldap_attributes_to_retrieve', array() );
1282 + $ldap_attributes_to_retrieve = array_merge( $ldap_attributes_to_retrieve, $additional_ldap_attributes_to_retrieve );
1283 +
685 1284 // Create default LDAP search filter. If LDAP email attribute is provided,
686 1285 // use (|(uid=$username)(mail=$username)) instead (so logins with either a
687 1286 // username or an email address will work). Otherwise use (uid=$username).
688 1287 if ( array_key_exists( 'ldap_attr_email', $auth_settings ) && strlen( $auth_settings['ldap_attr_email'] ) > 0 && substr( $auth_settings['ldap_attr_email'], 0, 1 ) !== '@' ) {
@@ -694,8 +1293,14 @@
694 1293 } else {
695 1294 $search_filter = '(' . $auth_settings['ldap_uid'] . '=' . $username . ')';
696 1295 }
697 1296
1297 + // Merge LDAP search filter from plugin settings if it exists.
1298 + $ldap_search_filter = trim( $auth_settings['ldap_search_filter'] );
1299 + if ( ! empty( $ldap_search_filter ) ) {
1300 + $search_filter = '(&' . $search_filter . $ldap_search_filter . ')';
1301 + }
1302 +
698 1303 /**
699 1304 * Filter LDAP search filter.
700 1305 *
701 1306 * Allows for custom LDAP authentication rules (e.g., restricting login
@@ -706,24 +1311,40 @@
706 1311 * @param string $username The username attempting to log in.
707 1312 */
708 1313 $search_filter = apply_filters( 'authorizer_ldap_search_filter', $search_filter, $auth_settings['ldap_uid'], $username );
709 1314
1315 + if ( is_array( $debug ) ) {
1316 + /* TRANSLATORS: LDAP search filter */
1317 + $debug[] = sprintf( __( 'Using LDAP search filter: %s', 'authorizer' ), $search_filter );
1318 + }
1319 +
710 1320 // Multiple search bases can be provided, so iterate through them until a match is found.
711 1321 foreach ( $search_bases as $search_base ) {
712 - $ldap_search = ldap_search(
1322 + $ldap_search = @ldap_search( // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
713 1323 $ldap,
714 1324 $search_base,
715 1325 $search_filter,
716 1326 $ldap_attributes_to_retrieve
717 1327 );
718 - $ldap_entries = ldap_get_entries( $ldap, $ldap_search );
1328 + $ldap_entries = empty( $ldap_search ) ? array( 'count' => 0 ) : ldap_get_entries( $ldap, $ldap_search );
719 1329 if ( $ldap_entries['count'] > 0 ) {
1330 + if ( is_array( $debug ) ) {
1331 + /* TRANSLATORS: 1: LDAP user 2: LDAP search base */
1332 + $debug[] = sprintf( __( 'Found user %1$s in search base: %2$s', 'authorizer' ), $username, $search_base );
1333 + }
720 1334 break;
1335 + } elseif ( is_array( $debug ) ) {
1336 + /* TRANSLATORS: 1: LDAP user 2: LDAP search base */
1337 + $debug[] = sprintf( __( 'Failed to find user %1$s in %2$s. Trying next search base.', 'authorizer' ), $username, $search_base );
721 1338 }
722 1339 }
723 1340
724 1341 // If we didn't find any users in ldap, fall back to WordPress authentication.
725 1342 if ( $ldap_entries['count'] < 1 ) {
1343 + if ( is_array( $debug ) ) {
1344 + /* TRANSLATORS: LDAP User */
1345 + $debug[] = sprintf( __( 'Failed: no LDAP user %s found.', 'authorizer' ), $username );
1346 + }
726 1347 return null;
727 1348 }
728 1349
729 1350 // Get the bind dn and first/last names; if there are multiple results returned, just get the last one.
@@ -755,8 +1376,12 @@
755 1376 }
756 1377
757 1378 $result = @ldap_bind( $ldap, $ldap_user_dn, stripslashes( $password ) ); // phpcs:ignore
758 1379 if ( ! $result ) {
1380 + if ( is_array( $debug ) ) {
1381 + /* TRANSLATORS: LDAP User */
1382 + $debug[] = sprintf( __( 'Failed: password incorrect for LDAP user %s.', 'authorizer' ), $username );
1383 + }
759 1384 // We have a real ldap user, but an invalid password. Pass
760 1385 // through to wp authentication after failing LDAP (since
761 1386 // this could be a local account that happens to be the
762 1387 // same name as an LDAP user).
@@ -770,8 +1395,13 @@
770 1395 if ( strlen( $email ) > 0 ) {
771 1396 $externally_authenticated_email = Helper::lowercase( $email );
772 1397 }
773 1398
1399 + if ( is_array( $debug ) ) {
1400 + /* TRANSLATORS: 1: Current time 2: LDAP User 3: LDAP user email */
1401 + $debug[] = sprintf( __( '[%1$s] Successfully authenticated user %2$s (%3$s) via LDAP.', 'authorizer' ), wp_date( get_option( 'time_format' ) ), $username, $externally_authenticated_email );
1402 + }
1403 +
774 1404 return array(
775 1405 'email' => $externally_authenticated_email,
776 1406 'username' => $username,
777 1407 'first_name' => $first_name,
@@ -816,32 +1446,32 @@
816 1446
817 1447 // Reset option containing old error messages.
818 1448 delete_option( 'auth_settings_advanced_login_error' );
819 1449
820 - if ( session_id() === '' ) {
821 - session_start();
822 - }
823 -
824 1450 // If logged in to CAS, Log out of CAS.
825 1451 if ( 'cas' === self::$authenticated_by && '1' === $auth_settings['cas'] ) {
826 1452 if ( ! array_key_exists( 'PHPCAS_CLIENT', $GLOBALS ) || ! array_key_exists( 'phpCAS', $_SESSION ) ) {
827 1453
828 1454 /**
829 - * Get the CAS server version (default to SAML_VERSION_1_1).
1455 + * Get the CAS server protocol version (default to SAML 1.1).
830 1456 *
831 - * @see: https://developer.jasig.org/cas-clients/php/1.3.4/docs/api/group__public.html
1457 + * @see: https://apereo.github.io/phpCAS/api/group__public.html#gadea9415f40b8d2afc39f140c9be83bbe
832 1458 */
833 - $cas_version = SAML_VERSION_1_1;
834 - if ( 'CAS_VERSION_3_0' === $auth_settings['cas_version'] ) {
835 - $cas_version = CAS_VERSION_3_0;
836 - } elseif ( 'CAS_VERSION_2_0' === $auth_settings['cas_version'] ) {
837 - $cas_version = CAS_VERSION_2_0;
838 - } elseif ( 'CAS_VERSION_1_0' === $auth_settings['cas_version'] ) {
839 - $cas_version = CAS_VERSION_1_0;
840 - }
1459 + $cas_version = Options\External\Cas::get_instance()->sanitize_cas_version( $auth_settings['cas_version'] );
841 1460
1461 + /**
1462 + * Get valid service URLs for the CAS client to validate against.
1463 + *
1464 + * @see: https://github.com/apereo/phpCAS/security/advisories/GHSA-8q72-6qq8-xv64
1465 + */
1466 + $valid_base_urls = Options\External\Cas::get_instance()->get_valid_cas_service_urls();
1467 +
842 1468 // Set the CAS client configuration if it hasn't been set already.
843 - \phpCAS::client( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'] );
1469 + if ( 'PROXY' === strtoupper( $auth_settings['cas_method'] ) ) {
1470 + \phpCAS::proxy( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
1471 + } else {
1472 + \phpCAS::client( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
1473 + }
844 1474 // Allow redirects at the CAS server endpoint (e.g., allow connections
845 1475 // at an old CAS URL that redirects to a newer CAS URL).
846 1476 \phpCAS::setExtraCurlOption( CURLOPT_FOLLOWLOCATION, true );
847 1477 // Restrict logout request origin to the CAS server only (prevent DDOS).
@@ -858,38 +1488,51 @@
858 1488 }
859 1489 }
860 1490
861 1491 // If session token set, log out of Google.
862 - if ( 'google' === self::$authenticated_by || array_key_exists( 'token', $_SESSION ) ) {
1492 + if ( session_id() === '' ) {
1493 + session_start();
1494 + }
1495 + if ( 'google' === self::$authenticated_by && array_key_exists( 'token', $_SESSION ) ) {
863 1496 $token = $_SESSION['token'];
864 1497
865 - // Edge case: if another plugin has already defined the Google_Client class,
866 - // and it's a version earlier than v2, then we need to handle $token as a
867 - // json-encoded string instead of an array.
868 - if ( ! is_array( $token ) ) {
869 - $token = json_decode( $token, true );
1498 + // Fetch the Google Client ID (allow overrides from filter or constant).
1499 + if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_ID' ) ) {
1500 + $auth_settings['google_clientid'] = \AUTHORIZER_GOOGLE_CLIENT_ID;
870 1501 }
1502 + /**
1503 + * Filters the Google Client ID used by Authorizer to authenticate.
1504 + *
1505 + * @since 3.9.0
1506 + *
1507 + * @param string $google_client_id The stored Google Client ID.
1508 + */
1509 + $auth_settings['google_clientid'] = apply_filters( 'authorizer_google_client_id', $auth_settings['google_clientid'] );
871 1510
872 - $access_token = isset( $token['access_token'] ) ? $token['access_token'] : null;
873 -
874 - // Add Google API PHP Client.
875 - // @see https://github.com/google/google-api-php-client branch:v1-master.
876 - if ( ! class_exists( 'Google_Client' ) ) {
877 - require_once dirname( plugin_root() ) . '/vendor/google-api-php-client-v2/src/Google/autoload.php';
1511 + // Fetch the Google Client Secret (allow overrides from filter or constant).
1512 + if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_SECRET' ) ) {
1513 + $auth_settings['google_clientsecret'] = \AUTHORIZER_GOOGLE_CLIENT_SECRET;
878 1514 }
1515 + /**
1516 + * Filters the Google Client Secret used by Authorizer to authenticate.
1517 + *
1518 + * @since 3.6.1
1519 + *
1520 + * @param string $google_client_secret The stored Google Client Secret.
1521 + */
1522 + $auth_settings['google_clientsecret'] = apply_filters( 'authorizer_google_client_secret', $auth_settings['google_clientsecret'] );
879 1523
880 1524 // Build the Google Client.
881 1525 $client = new \Google_Client();
882 1526 $client->setApplicationName( 'WordPress' );
883 - $client->setClientId( $auth_settings['google_clientid'] );
884 - $client->setClientSecret( $auth_settings['google_clientsecret'] );
1527 + $client->setClientId( trim( $auth_settings['google_clientid'] ) );
1528 + $client->setClientSecret( trim( $auth_settings['google_clientsecret'] ) );
885 1529 $client->setRedirectUri( 'postmessage' );
886 1530
887 1531 // Revoke the token.
888 - $client->revokeToken( $access_token );
1532 + $client->revokeToken( $token );
889 1533
890 1534 // Remove the credentials from the user's session.
891 1535 unset( $_SESSION['token'] );
892 1536 }
893 1537 }
894 -
895 1538 }