PluginProbe
Authorizer / 3.13.0
Authorizer v3.13.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 2.9.6 All 126 releases
authorizer / src / authorizer / class-authentication.php

class-authentication.php in Authorizer 3.13.0, at src/authorizer/class-authentication.php

2,211 lines 91.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Authorizer
4 *
5 * @license GPL-2.0+
6 * @link https://github.com/uhm-coe/authorizer
7 * @package authorizer
8 */
9
10 namespace Authorizer;
11
12 use Authorizer\Helper;
13 use Authorizer\Options;
14 use Authorizer\Authorization;
15
16 /**
17 * Implements the authentication (is user who they say they are?) features of
18 * the plugin.
19 */
20 class Authentication extends Singleton {
21
22 /**
23 * Tracks the external service used by the user currently logging out.
24 *
25 * @var string
26 */
27 private static $authenticated_by = '';
28
29 /**
30 * Authenticate against an external service.
31 *
32 * Filter: authenticate
33 *
34 * @param WP_User $user user to authenticate.
35 * @param string $username optional username to authenticate.
36 * @param string $password optional password to authenticate.
37 * @return WP_User|WP_Error WP_User on success, WP_Error on failure.
38 */
39 public function custom_authenticate( $user, $username, $password ) {
40 // Pass through if already authenticated.
41 if ( is_a( $user, 'WP_User' ) ) {
42 return $user;
43 } else {
44 $user = null;
45 }
46
47 // If username and password are blank, this isn't a log in attempt.
48 $is_login_attempt = strlen( $username ) > 0 && strlen( $password ) > 0;
49
50 // Check to make sure that $username is not locked out due to too
51 // many invalid login attempts. If it is, tell the user how much
52 // time remains until they can try again.
53 $unauthenticated_user = $is_login_attempt ? get_user_by( 'login', $username ) : false;
54 $unauthenticated_user_is_blocked = false;
55 if ( $is_login_attempt && false !== $unauthenticated_user ) {
56 $last_attempt = get_user_meta( $unauthenticated_user->ID, 'auth_settings_advanced_lockouts_time_last_failed', true );
57 $num_attempts = get_user_meta( $unauthenticated_user->ID, 'auth_settings_advanced_lockouts_failed_attempts', true );
58 // Also check the auth_blocked user_meta flag (users in blocked list will get this flag).
59 $unauthenticated_user_is_blocked = get_user_meta( $unauthenticated_user->ID, 'auth_blocked', true ) === 'yes';
60 } else {
61 $last_attempt = get_option( 'auth_settings_advanced_lockouts_time_last_failed' );
62 $num_attempts = get_option( 'auth_settings_advanced_lockouts_failed_attempts' );
63 }
64
65 // Inactive users should be treated like deleted users (we just
66 // do this to preserve any content they created, but here we should
67 // pretend they don't exist).
68 if ( $unauthenticated_user_is_blocked ) {
69 remove_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
70 remove_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
71 return new \WP_Error( 'empty_password', __( '<strong>ERROR</strong>: Incorrect username or password.', 'authorizer' ) );
72 }
73
74 // Grab plugin settings.
75 $options = Options::get_instance();
76 $auth_settings = $options->get_all( Helper::SINGLE_CONTEXT, 'allow override' );
77
78 // Make sure $last_attempt (time) and $num_attempts are positive integers.
79 // Note: this addresses resetting them if either is unset from above.
80 $last_attempt = absint( $last_attempt );
81 $num_attempts = absint( $num_attempts );
82
83 // Create semantic lockout variables.
84 $lockouts = $auth_settings['advanced_lockouts'];
85 $time_since_last_fail = time() - $last_attempt;
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;
91
92 // Check if we need to institute a lockout delay.
93 if ( $is_login_attempt && $time_since_last_fail > $reset_duration ) {
94 // Enough time has passed since the last invalid attempt and
95 // now that we can reset the failed attempt count, and let this
96 // login attempt go through.
97 $num_attempts = 0; // This does nothing, but include it for semantic meaning.
98 } elseif ( $is_login_attempt && $num_attempts > $num_attempts_long_lockout && $seconds_remaining_long_lockout > 0 ) {
99 // Stronger lockout (1st/2nd round of invalid attempts reached)
100 // Note: set the error code to 'empty_password' so it doesn't
101 // trigger the wp_login_failed hook, which would continue to
102 // increment the failed attempt count.
103 remove_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
104 remove_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
105 return new \WP_Error(
106 'empty_password',
107 sprintf(
108 /* TRANSLATORS: 1: username 2: duration of lockout in seconds 3: duration of lockout as a phrase 4: lost password URL */
109 __( '<strong>ERROR</strong>: There have been too many invalid login attempts for the username <strong>%1$s</strong>. Please wait <strong id="seconds_remaining" data-seconds="%2$s">%3$s</strong> before trying again. <a href="%4$s" title="Password Lost and Found">Lost your password</a>?', 'authorizer' ),
110 $username,
111 $seconds_remaining_long_lockout,
112 Helper::seconds_as_sentence( $seconds_remaining_long_lockout ),
113 wp_lostpassword_url()
114 )
115 );
116 } elseif ( $is_login_attempt && $num_attempts > $num_attempts_short_lockout && $seconds_remaining_short_lockout > 0 ) {
117 // Normal lockout (1st round of invalid attempts reached)
118 // Note: set the error code to 'empty_password' so it doesn't
119 // trigger the wp_login_failed hook, which would continue to
120 // increment the failed attempt count.
121 remove_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
122 remove_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
123 return new \WP_Error(
124 'empty_password',
125 sprintf(
126 /* TRANSLATORS: 1: username 2: duration of lockout in seconds 3: duration of lockout as a phrase 4: lost password URL */
127 __( '<strong>ERROR</strong>: There have been too many invalid login attempts for the username <strong>%1$s</strong>. Please wait <strong id="seconds_remaining" data-seconds="%2$s">%3$s</strong> before trying again. <a href="%4$s" title="Password Lost and Found">Lost your password</a>?', 'authorizer' ),
128 $username,
129 $seconds_remaining_short_lockout,
130 Helper::seconds_as_sentence( $seconds_remaining_short_lockout ),
131 wp_lostpassword_url()
132 )
133 );
134 }
135
136 // Start external authentication.
137 $externally_authenticated_emails = array();
138 $authenticated_by = '';
139 $result = null;
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
159 // Try OIDC authentication if it's enabled and we don't have a
160 // successful login yet.
161 if (
162 '1' === $auth_settings['oidc'] &&
163 0 === count( $externally_authenticated_emails ) &&
164 ! is_wp_error( $result )
165 ) {
166 $result = $this->custom_authenticate_oidc( $auth_settings );
167 if ( ! is_null( $result ) && ! is_wp_error( $result ) ) {
168 if ( is_array( $result['email'] ) ) {
169 $externally_authenticated_emails = $result['email'];
170 } else {
171 $externally_authenticated_emails[] = $result['email'];
172 }
173 $authenticated_by = $result['authenticated_by'];
174 }
175 }
176
177 // Try Google authentication if it's enabled and we don't have a
178 // successful login yet.
179 if (
180 '1' === $auth_settings['google'] &&
181 0 === count( $externally_authenticated_emails ) &&
182 ! is_wp_error( $result )
183 ) {
184 $result = $this->custom_authenticate_google( $auth_settings );
185 if ( ! is_null( $result ) && ! is_wp_error( $result ) ) {
186 if ( is_array( $result['email'] ) ) {
187 $externally_authenticated_emails = $result['email'];
188 } else {
189 $externally_authenticated_emails[] = $result['email'];
190 }
191 $authenticated_by = $result['authenticated_by'];
192 }
193 }
194
195 // Try CAS authentication if it's enabled and we don't have a
196 // successful login yet.
197 if (
198 '1' === $auth_settings['cas'] &&
199 0 === count( $externally_authenticated_emails ) &&
200 ! is_wp_error( $result )
201 ) {
202 $result = $this->custom_authenticate_cas( $auth_settings );
203 if ( ! is_null( $result ) && ! is_wp_error( $result ) ) {
204 if ( is_array( $result['email'] ) ) {
205 $externally_authenticated_emails = $result['email'];
206 } else {
207 $externally_authenticated_emails[] = $result['email'];
208 }
209 $authenticated_by = $result['authenticated_by'];
210 }
211 }
212
213 // Try LDAP authentication if it's enabled and we don't have an
214 // authenticated user yet.
215 if (
216 '1' === $auth_settings['ldap'] &&
217 0 === count( $externally_authenticated_emails ) &&
218 ! is_wp_error( $result )
219 ) {
220 $result = $this->custom_authenticate_ldap( $auth_settings, $username, $password );
221 if ( ! is_null( $result ) && ! is_wp_error( $result ) ) {
222 if ( is_array( $result['email'] ) ) {
223 $externally_authenticated_emails = $result['email'];
224 } else {
225 $externally_authenticated_emails[] = $result['email'];
226 }
227 $authenticated_by = $result['authenticated_by'];
228 }
229 }
230
231 // If we don't have an externally authenticated user, either skip to
232 // WordPress authentication (if WordPress logins are enabled), or return
233 // an error (if WordPress logins are disabled and at least one external
234 // service is enabled).
235 if ( count( array_filter( $externally_authenticated_emails ) ) < 1 ) {
236 if (
237 array_key_exists( 'advanced_disable_wp_login', $auth_settings ) &&
238 '1' === $auth_settings['advanced_disable_wp_login'] &&
239 (
240 '1' === $auth_settings['oauth2'] ||
241 '1' === $auth_settings['oidc'] ||
242 '1' === $auth_settings['google'] ||
243 '1' === $auth_settings['cas'] ||
244 '1' === $auth_settings['ldap']
245 )
246 ) {
247 // Edge case: if WordPress logins are disabled but the username/email
248 // attempting to login has been added to the list of users allowed to
249 // bypass disabled logins, then allow the login (proceed to WordPress
250 // authentication).
251 if ( ! empty( $auth_settings['advanced_disable_wp_login_bypass_usernames'] ) ) {
252 $bypass_usernames = explode( "\n", str_replace( "\r", '', $auth_settings['advanced_disable_wp_login_bypass_usernames'] ) );
253 $bypass_users = get_users( array(
254 'login__in' => $bypass_usernames,
255 'count_total' => false,
256 ) );
257 foreach ( $bypass_users as $bypass_user ) {
258 if ( $bypass_user->user_login === $username || $bypass_user->user_email === $username ) {
259 return null;
260 }
261 }
262 }
263
264 remove_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
265 remove_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
266
267 $error = new \WP_Error();
268
269 if ( empty( $username ) ) {
270 $error->add( 'empty_username', __( '<strong>ERROR</strong>: The username field is empty.', 'authorizer' ) );
271 }
272
273 if ( empty( $password ) ) {
274 $error->add( 'empty_password', __( '<strong>ERROR</strong>: The password field is empty.', 'authorizer' ) );
275 }
276
277 return $error;
278 }
279
280 return $result;
281 }
282
283 // Remove duplicate and blank emails, if any.
284 $externally_authenticated_emails = array_filter( array_unique( $externally_authenticated_emails ) );
285
286 /**
287 * If we've made it this far, we should have an externally
288 * authenticated user. The following should be set:
289 * $externally_authenticated_emails
290 * $authenticated_by
291 */
292
293 // Look for an existing WordPress account matching the externally
294 // authenticated user. Perform the match either by username or email.
295 $link_on_username = false;
296 if ( 'cas' === $authenticated_by ) {
297 // Check the specific CAS server's link_on_username setting.
298 $cas_server_id = isset( $result['cas_server_id'] ) ? intval( $result['cas_server_id'] ) : 1;
299 $suffix = $cas_server_id > 1 ? '_' . $cas_server_id : '';
300 $cas_link_on_username_key = 'cas_link_on_username' . $suffix;
301 if ( isset( $auth_settings[ $cas_link_on_username_key ] ) && 1 === intval( $auth_settings[ $cas_link_on_username_key ] ) ) {
302 $link_on_username = true;
303 }
304 } elseif ( 'oidc' === $authenticated_by ) {
305 // Check the specific OIDC server's link_on_username setting.
306 $oidc_server_id = isset( $result['oidc_server_id'] ) ? intval( $result['oidc_server_id'] ) : 1;
307 $suffix = $oidc_server_id > 1 ? '_' . $oidc_server_id : '';
308 $oidc_link_on_username_key = 'oidc_link_on_username' . $suffix;
309 if ( isset( $auth_settings[ $oidc_link_on_username_key ] ) && 1 === intval( $auth_settings[ $oidc_link_on_username_key ] ) ) {
310 $link_on_username = true;
311 }
312 }
313
314 if ( $link_on_username ) {
315 // Get the external user's WordPress account by username. This is less
316 // secure, but a user reported having an installation where a previous
317 // CAS plugin had created over 9000 WordPress accounts without email
318 // addresses. This option was created to support that case, and any
319 // other CAS/OIDC servers where emails are not used as account identifiers.
320 $user = get_user_by( 'login', $result['username'] );
321 } else {
322 // Get the external user's WordPress account by email address. This is
323 // the normal behavior (and the most secure).
324 foreach ( $externally_authenticated_emails as $externally_authenticated_email ) {
325 $user = get_user_by( 'email', Helper::lowercase( $externally_authenticated_email ) );
326 // Stop trying email addresses once we have found a match.
327 if ( false !== $user ) {
328 break;
329 }
330 }
331 }
332
333 // Always clean up OIDC session variables after authentication attempt,
334 // regardless of user lookup result. This prevents session pollution if OIDC
335 // authentication succeeds but user verification fails.
336 // Note: oidc_redirect_to is preserved here and cleaned up later in
337 // maybe_redirect_after_oidc_login() after it's used for the login redirect.
338 if ( 'oidc' === $authenticated_by && PHP_SESSION_NONE !== session_status() ) {
339 unset( $_SESSION['oidc_server_id'] );
340 }
341
342 // Check this external user's access against the access lists
343 // (pending, approved, blocked).
344 $check_user_access_result = Authorization::get_instance()->check_user_access(
345 $user,
346 $externally_authenticated_emails,
347 $result
348 );
349
350 // Fail with message if there was an error creating/adding the user.
351 if ( is_wp_error( $check_user_access_result ) || 0 === $check_user_access_result ) {
352 // Clean up oidc_redirect_to if access check fails (redirect filter won't run).
353 if ( 'oidc' === $authenticated_by && PHP_SESSION_NONE !== session_status() ) {
354 unset( $_SESSION['oidc_redirect_to'] );
355 }
356
357 return $check_user_access_result;
358 }
359
360 // If we have a valid user from check_user_access(), log that user in.
361 if ( get_class( $check_user_access_result ) === 'WP_User' ) {
362 $user = $check_user_access_result;
363 }
364
365 // If this is an OIDC login, update OIDC user meta for the successfully
366 // logged in user.
367 if ( $user && 'oidc' === $authenticated_by ) {
368 // Always store server ID if present (needed to determine which OIDC server was used).
369 if ( isset( $result['oidc_server_id'] ) ) {
370 update_user_meta( $user->ID, 'oidc_server_id', intval( $result['oidc_server_id'] ) );
371 }
372 // Store ID token only if present and non-empty (needed for RP-initiated logout).
373 if ( ! empty( $result['oidc_id_token'] ) ) {
374 update_user_meta( $user->ID, 'oidc_id_token', $result['oidc_id_token'] );
375 }
376 }
377
378 // Integration: disable Cloudflare Turnstile verification from the
379 // simple-cloudflare-turnstile plugin if it is activated (conflicts with
380 // our redirects from external services). We assume that we dont't need bot
381 // protection from this plugin after coming back from a successful external
382 // service authentication.
383 add_filter( 'cfturnstile_widget_disable', '__return_true' );
384
385 // If we haven't exited yet, we have a valid/approved user, so authenticate them.
386 return $user;
387 }
388
389
390 /**
391 * Validate this user's credentials against selected OAuth2 provider.
392 *
393 * @param array $auth_settings Plugin settings.
394 * @return array|WP_Error Array containing email, authenticated_by, first_name,
395 * last_name, and username strings for the successfully
396 * authenticated user, or WP_Error() object on failure,
397 * or null if not attempting an oauth2 login.
398 */
399 protected function custom_authenticate_oauth2( $auth_settings ) {
400 // Move on if oauth2 hasn't been requested here or OAuth2 server ID is invalid.
401 if ( empty( $auth_settings['oauth2_num_servers'] ) ) {
402 $auth_settings['oauth2_num_servers'] = 1;
403 }
404
405 // Workaround: because Azure doesn't let us specify a querystring in a
406 // redirect_uri, we have to detect those redirects separately because we
407 // can't include external=oauth2 or id={oauth_server_id} in the URL.
408 // Instead, detect the absence of the `external` param, and the presence of
409 // `code` and `state` params.
410 if ( empty( $_GET['external'] ) && ! empty( $_GET['code'] ) && ! empty( $_GET['state'] ) ) {
411 // Fetch the OAuth2 server id from the session variable created during the
412 // initial request.
413 if ( PHP_SESSION_NONE === session_status() ) {
414 session_start();
415 }
416 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
417 $_GET['id'] = $_SESSION['oauth2_server_id'] ?? 1;
418 $_GET['external'] = 'oauth2';
419 }
420
421 // If this is an OAuth2 login attempt and the id param is missing, default
422 // it to 1.
423 if ( ! empty( $_GET['external'] ) && 'oauth2' === $_GET['external'] && empty( $_GET['id'] ) ) {
424 $_GET['id'] = 1;
425 }
426
427 // phpcs:ignore WordPress.Security.NonceVerification
428 if ( empty( $_GET['external'] ) || 'oauth2' !== $_GET['external'] || empty( $_GET['id'] ) || ! in_array( intval( $_GET['id'] ), range( 1, 20 ), true ) || intval( $_GET['id'] ) > intval( $auth_settings['oauth2_num_servers'] ) ) {
429 return null;
430 }
431
432 // Get the OAuth2 server id (since multiple OAuth2 servers can be configured),
433 // and the relevant settings for that server.
434 // phpcs:ignore WordPress.Security.NonceVerification
435 $oauth2_server_id = empty( $_GET['id'] ) ? 1 : intval( $_GET['id'] );
436 $suffix = $oauth2_server_id > 1 ? '_' . $oauth2_server_id : '';
437 $oauth2_provider = $auth_settings[ 'oauth2_provider' . $suffix ] ?? '';
438 $oauth2_clientid = $auth_settings[ 'oauth2_clientid' . $suffix ] ?? '';
439 $oauth2_clientsecret = $auth_settings[ 'oauth2_clientsecret' . $suffix ] ?? '';
440 $oauth2_hosteddomain = $auth_settings[ 'oauth2_hosteddomain' . $suffix ] ?? '';
441 $oauth2_tenant_id = $auth_settings[ 'oauth2_tenant_id' . $suffix ] ?? '';
442 $oauth2_url_authorize = $auth_settings[ 'oauth2_url_authorize' . $suffix ] ?? '';
443 $oauth2_url_token = $auth_settings[ 'oauth2_url_token' . $suffix ] ?? '';
444 $oauth2_url_resource = $auth_settings[ 'oauth2_url_resource' . $suffix ] ?? '';
445 $oauth2_attr_username = $auth_settings[ 'oauth2_attr_username' . $suffix ] ?? '';
446 $oauth2_attr_email = $auth_settings[ 'oauth2_attr_email' . $suffix ] ?? '';
447 $oauth2_attr_first_name = $auth_settings[ 'oauth2_attr_first_name' . $suffix ] ?? '';
448 $oauth2_attr_last_name = $auth_settings[ 'oauth2_attr_last_name' . $suffix ] ?? '';
449
450 // Fetch the Oauth2 Client ID (allow overrides from filter or constant).
451 // Note: constant/filter overrides are only supported for a single OAuth2 server.
452 if ( defined( 'AUTHORIZER_OAUTH2_CLIENT_ID' ) ) {
453 $oauth2_clientid = \AUTHORIZER_OAUTH2_CLIENT_ID;
454 }
455 /**
456 * Filters the Oauth2 Client ID used by Authorizer to authenticate.
457 *
458 * @since 3.9.0
459 *
460 * @param string $oauth2_client_id The stored Oauth2 Client ID.
461 */
462 $oauth2_clientid = apply_filters( 'authorizer_oauth2_client_id', $oauth2_clientid );
463
464 // Fetch the Oauth2 Client Secret (allow overrides from filter or constant).
465 // Note: constant/filter overrides are only supported for a single OAuth2 server.
466 if ( defined( 'AUTHORIZER_OAUTH2_CLIENT_SECRET' ) ) {
467 $oauth2_clientsecret = \AUTHORIZER_OAUTH2_CLIENT_SECRET;
468 }
469 /**
470 * Filters the Oauth2 Client Secret used by Authorizer to authenticate.
471 *
472 * @since 3.6.1
473 *
474 * @param string $oauth2_client_secret The stored Oauth2 Client Secret.
475 */
476 $oauth2_clientsecret = apply_filters( 'authorizer_oauth2_client_secret', $oauth2_clientsecret );
477
478 // Move on if required params aren't specified in settings.
479 if (
480 empty( $oauth2_clientid ) ||
481 empty( $oauth2_clientsecret )
482 ) {
483 return null;
484 }
485
486 // Build the redirectUri for the OAuth2 provider to redirect back to.
487 // Note: omit the id param if it is 1 (default server) for backwards
488 // compatibility with installations already configured before Authorizer
489 // supported multiple OAuth2 servers (so it doesn't break existing
490 // redirectUris authorized on the external service).
491 $redirect_uri = site_url( '/wp-login.php?external=oauth2' );
492 if ( $oauth2_server_id > 1 ) {
493 $redirect_uri .= '&id=' . $oauth2_server_id;
494 }
495 if ( 'azure' === $oauth2_provider ) {
496 // Microsoft Azure does not support querystrings in the redirectUri, so
497 // we have to use the base wp-login.php URL. We save parameters in the
498 // session instead (see below).
499 $redirect_uri = site_url( '/wp-login.php' );
500 }
501
502 // Authenticate with GitHub.
503 // See: https://github.com/thephpleague/oauth2-github.
504 if ( 'github' === $oauth2_provider ) {
505 if ( PHP_SESSION_NONE === session_status() ) {
506 session_start();
507 }
508 $provider = new \League\OAuth2\Client\Provider\Github( array(
509 'clientId' => $oauth2_clientid,
510 'clientSecret' => $oauth2_clientsecret,
511 'redirectUri' => $redirect_uri,
512 ) );
513
514 // If we don't have an authorization code, then get one.
515 if ( ! isset( $_REQUEST['code'] ) ) {
516 $auth_url = $provider->getAuthorizationUrl( array(
517 'scope' => 'user:email',
518 ) );
519 $_SESSION['oauth2state'] = $provider->getState();
520 header( 'Location: ' . $auth_url );
521 exit;
522
523 } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
524 // Check state against previously stored one to mitigate CSRF attacks.
525 unset( $_SESSION['oauth2state'] );
526 exit;
527
528 } else {
529 // Try to get an access token (using the authorization code grant).
530 try {
531 $token = $provider->getAccessToken( 'authorization_code', array(
532 'code' => $_REQUEST['code'],
533 ) );
534 } catch ( \Exception $e ) {
535 // Failed to get token; try again from the beginning. Usually a
536 // 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.
537 $auth_url = $provider->getAuthorizationUrl( array(
538 'scope' => 'user:email',
539 ) );
540 $_SESSION['oauth2state'] = $provider->getState();
541
542 // Log the error for debugging.
543 error_log( __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ) ); // phpcs:ignore
544 error_log( $e->getMessage() ); // phpcs:ignore
545
546 // Also log the error to the Simple History plugin (if it is active).
547 apply_filters(
548 'simple_history_log_warning',
549 __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ),
550 array(
551 'error' => $e->getMessage(),
552 )
553 );
554
555 header( 'Location: ' . $auth_url );
556 exit;
557 }
558
559 try {
560 // Look up user using token.
561 $user = $provider->getResourceOwner( $token );
562
563 $email = $user->getEmail();
564 $username = $user->getNickname();
565 $attributes = $user->toArray();
566
567 // If user has no public email, fetch all emails and use those.
568 if ( empty( $email ) ) {
569 $request = $provider->getAuthenticatedRequest(
570 'GET',
571 $provider->getResourceOwnerDetailsUrl( $token ) . '/emails',
572 $token
573 );
574 $attributes['emails'] = array_filter( array_map(
575 function ( $entry ) {
576 return empty( $entry['email'] ) ? '' : $entry['email'];
577 },
578 (array) $provider->getParsedResponse( $request )
579 ) );
580 $email = $attributes['emails'];
581 }
582 } catch ( \Exception $e ) {
583 // Failed to get user details.
584 return null;
585 }
586 }
587 } elseif ( 'azure' === $oauth2_provider ) {
588 // Authenticate with the Microsoft Azure oauth2 client.
589 // See: https://github.com/thenetworg/oauth2-azure.
590 if ( PHP_SESSION_NONE === session_status() ) {
591 session_start();
592 }
593 try {
594 // Save the redirect URL for WordPress so we can restore it after a
595 // successful login (note: we can't add the redirect_to querystring
596 // param to the redirectUri param below because it won't match the
597 // approved URI set in the Azure portal).
598 $login_querystring = array();
599 if ( isset( $_SERVER['QUERY_STRING'] ) ) {
600 parse_str( $_SERVER['QUERY_STRING'], $login_querystring ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
601 }
602 if ( isset( $login_querystring['redirect_to'] ) ) {
603 $_SESSION['oauth2_redirect_to'] = $login_querystring['redirect_to'];
604 }
605 // Save the OAuth2 server id so we can restore it after a successful
606 // login (note: we can't add the id querystring param to the redirectUri
607 // param below because it won't match the approved URI set in the Azure
608 // portal).
609 $_SESSION['oauth2_server_id'] = $oauth2_server_id;
610
611 $provider = new \TheNetworg\OAuth2\Client\Provider\Azure( array(
612 'clientId' => $oauth2_clientid,
613 'clientSecret' => $oauth2_clientsecret,
614 'redirectUri' => $redirect_uri,
615 'tenant' => empty( $oauth2_tenant_id ) ? 'common' : $oauth2_tenant_id,
616 ) );
617 // Use v2 API. Set to Azure::ENDPOINT_VERSION_1_0 to use v1 API.
618 $provider->defaultEndPointVersion = \TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;
619
620 $baseGraphUri = $provider->getRootMicrosoftGraphUri( null );
621 $provider->scope = 'openid profile email offline_access ' . $baseGraphUri . '/User.Read';
622 } catch ( \Exception $e ) {
623 // Invalid configuration, so this in not a successful login. Show error
624 // message to user.
625 return new \WP_Error( 'empty_username', $e->getMessage() );
626 }
627
628 // If we don't have an authorization code, then get one.
629 if ( ! isset( $_REQUEST['code'] ) ) {
630 try {
631 $auth_url = $provider->getAuthorizationUrl( array(
632 'scope' => $provider->scope,
633 ) );
634 $_SESSION['oauth2state'] = $provider->getState();
635 header( 'Location: ' . $auth_url );
636 exit;
637 } catch ( \Exception $e ) {
638 // Invalid configuration, so this in not a successful login. Show error
639 // message to user.
640 return new \WP_Error( 'empty_username', $e->getMessage() );
641 }
642 } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
643 // Check state against previously stored one to mitigate CSRF attacks.
644 unset( $_SESSION['oauth2state'] );
645 exit;
646 } else {
647 // Try to get an access token (using the authorization code grant).
648 try {
649 $token = $provider->getAccessToken( 'authorization_code', array(
650 'code' => $_REQUEST['code'],
651 'scope' => $provider->scope,
652 ) );
653 } catch ( \Exception $e ) {
654 // Failed to get token; try again from the beginning.
655 $auth_url = $provider->getAuthorizationUrl( array(
656 'scope' => $provider->scope,
657 ) );
658 $_SESSION['oauth2state'] = $provider->getState();
659
660 // Log the error for debugging.
661 error_log( __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ) ); // phpcs:ignore
662 error_log( $e->getMessage() ); // phpcs:ignore
663
664 // Also log the error to the Simple History plugin (if it is active).
665 apply_filters(
666 'simple_history_log_warning',
667 __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ),
668 array(
669 'error' => $e->getMessage(),
670 )
671 );
672
673 header( 'Location: ' . $auth_url );
674 exit;
675 }
676
677 try {
678 // Look up user using token.
679 $user = $provider->getResourceOwner( $token );
680
681 $attributes = $user->toArray();
682 $email = empty( $attributes['email'] ) ? '' : $attributes['email'];
683 $username = empty( $attributes['preferred_username'] ) ? '' : $attributes['preferred_username'];
684
685 // Attempt to find an email address in the resource owner attributes
686 // if we couldn't find one in the `email` attribute.
687 if ( empty( $email ) ) {
688 $email = Helper::find_emails_in_multi_array( $attributes );
689 }
690 } catch ( \Exception $e ) {
691 // Failed to get user details.
692 return null;
693 }
694
695 /**
696 * Filter the generic oauth2 authenticated user email.
697 *
698 * @param string $email Discovered email (or empty string).
699 *
700 * @param array $attributes Resource Owner attributes returned from oauth2 endpoint.
701 */
702 $email = apply_filters( 'authorizer_oauth2_generic_authenticated_email', $email, $attributes );
703
704 /**
705 * Filter the azure oauth2 authenticated user email.
706 *
707 * @param string $email Discovered email (or empty string).
708 *
709 * @param array $attributes Resource Owner attributes returned from oauth2 endpoint.
710 */
711 $email = apply_filters( 'authorizer_oauth2_azure_authenticated_email', $email, $attributes );
712
713 // Set the username to the email prefix (if we don't have one).
714 if ( ! empty( $email ) && empty( $username ) ) {
715 if ( is_array( $email ) && ! empty( $email[0] ) ) {
716 $username = current( explode( '@', $email[0] ) );
717 } else {
718 $username = current( explode( '@', $email ) );
719 }
720 }
721 }
722 } elseif ( 'generic' === $oauth2_provider ) {
723 // Authenticate with the generic oauth2 client.
724 // See: https://github.com/thephpleague/oauth2-client.
725 // Move on if required params aren't specified in settings.
726 if (
727 empty( $oauth2_url_authorize ) ||
728 empty( $oauth2_url_token ) ||
729 empty( $oauth2_url_resource )
730 ) {
731 return null;
732 }
733
734 if ( PHP_SESSION_NONE === session_status() ) {
735 session_start();
736 }
737 // Save the redirect URL for WordPress so we can restore it after a
738 // successful login (note: many OAuth2 providers discard the param).
739 $login_querystring = array();
740 if ( isset( $_SERVER['QUERY_STRING'] ) ) {
741 parse_str( $_SERVER['QUERY_STRING'], $login_querystring ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
742 }
743 if ( isset( $login_querystring['redirect_to'] ) ) {
744 $_SESSION['oauth2_redirect_to'] = $login_querystring['redirect_to'];
745 }
746
747 $provider = new \League\OAuth2\Client\Provider\GenericProvider( array(
748 'clientId' => $oauth2_clientid,
749 'clientSecret' => $oauth2_clientsecret,
750 'redirectUri' => $redirect_uri,
751 'urlAuthorize' => $oauth2_url_authorize,
752 'urlAccessToken' => $oauth2_url_token,
753 'urlResourceOwnerDetails' => $oauth2_url_resource,
754 ) );
755
756 // If we don't have an authorization code, then get one.
757 if ( ! isset( $_REQUEST['code'] ) ) {
758 $auth_url = $provider->getAuthorizationUrl(
759 /**
760 * Filter the parameters passed to the generic oauth2 authorization endpoint.
761 *
762 * @param array() $params Array of key/value pairs where keys represent
763 * a GET param and value is its value.
764 */
765 apply_filters( 'authorizer_oauth2_generic_authorization_parameters', array() )
766 );
767 $_SESSION['oauth2state'] = $provider->getState();
768 header( 'Location: ' . $auth_url );
769 exit;
770 } elseif ( empty( $_REQUEST['state'] ) || empty( $_SESSION['oauth2state'] ) || $_REQUEST['state'] !== $_SESSION['oauth2state'] ) {
771 // Check state against previously stored one to mitigate CSRF attacks.
772 unset( $_SESSION['oauth2state'] );
773 exit;
774 } else {
775 // Try to get an access token (using the authorization code grant).
776 try {
777 $token = $provider->getAccessToken( 'authorization_code', array(
778 'code' => $_REQUEST['code'],
779 ) );
780 } catch ( \Exception $e ) {
781 // Failed to get token; try again from the beginning.
782 $auth_url = $provider->getAuthorizationUrl(
783 /**
784 * Filter the parameters passed to the generic oauth2 authorization endpoint.
785 *
786 * @param array() $params Array of key/value pairs where keys represent
787 * a GET param and value is its value.
788 */
789 apply_filters( 'authorizer_oauth2_generic_authorization_parameters', array() )
790 );
791 $_SESSION['oauth2state'] = $provider->getState();
792
793 // Log the error for debugging.
794 error_log( __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ) ); // phpcs:ignore
795 error_log( $e->getMessage() ); // phpcs:ignore
796
797 // Also log the error to the Simple History plugin (if it is active).
798 apply_filters(
799 'simple_history_log_warning',
800 __( 'OAuth2 server returned an Exception. Details:', 'authorizer' ),
801 array(
802 'error' => $e->getMessage(),
803 )
804 );
805
806 header( 'Location: ' . $auth_url );
807 exit;
808 }
809
810 try {
811 // Look up user using token.
812 $user = $provider->getResourceOwner( $token );
813
814 $email = '';
815 $username = '';
816 $attributes = $user->toArray();
817
818 // Attempt to find an email address in the resource owner attributes.
819 $email = Helper::find_emails_in_multi_array( $attributes );
820 } catch ( \Exception $e ) {
821 // Failed to get user details.
822 return null;
823 }
824
825 // Get custom username attribute, if specified (handle string or array results from attribute).
826 if ( ! empty( $oauth2_attr_username ) && ! empty( $attributes[ $oauth2_attr_username ] ) ) {
827 if ( is_string( $attributes[ $oauth2_attr_username ] ) ) {
828 $username = trim( $attributes[ $oauth2_attr_username ] );
829 } elseif ( is_array( $attributes[ $oauth2_attr_username ] ) ) {
830 $username = trim( array_shift( $attributes[ $oauth2_attr_username ] ) );
831 }
832 }
833
834 // Get custom email attribute, if specified.
835 if ( ! empty( $oauth2_attr_email ) && ! empty( $attributes[ $oauth2_attr_email ] ) ) {
836 if ( is_string( $attributes[ $oauth2_attr_email ] ) ) {
837 $email = trim( $attributes[ $oauth2_attr_email ] );
838 } elseif ( is_array( $attributes[ $oauth2_attr_email ] ) ) {
839 $email = $attributes[ $oauth2_attr_email ];
840 }
841 }
842
843 /**
844 * Filter the generic oauth2 authenticated user email.
845 *
846 * @param string|array $email Discovered email or array of emails (or empty string).
847 * @param array $attributes Resource Owner attributes returned from oauth2 endpoint.
848 */
849 $email = apply_filters( 'authorizer_oauth2_generic_authenticated_email', $email, $attributes );
850
851 // Set the username to the email prefix (if we don't have one).
852 if ( ! empty( $email ) && empty( $username ) ) {
853 if ( is_array( $email ) && ! empty( $email[0] ) ) {
854 $username = current( explode( '@', $email[0] ) );
855 } else {
856 $username = current( explode( '@', $email ) );
857 }
858 }
859 }
860 } else {
861 // Move on if a supported providers wasn't selected.
862 return null;
863 }
864
865 // Make sure email is lowercase.
866 if ( is_array( $email ) ) {
867 $externally_authenticated_email = array();
868 foreach ( $email as $external_email ) {
869 $externally_authenticated_email[] = Helper::lowercase( $external_email );
870 }
871 } else {
872 $externally_authenticated_email = array_filter( array( Helper::lowercase( $email ) ) );
873 }
874
875 // Move on if no emails were found.
876 if ( empty( $externally_authenticated_email ) ) {
877 return null;
878 }
879
880 /**
881 * Fail if hosteddomain param is set and the logging in user's email address
882 * doesn't match the allowed hosted domain.
883 */
884 if (
885 array_key_exists( 'oauth2_hosteddomain', $auth_settings ) &&
886 strlen( $oauth2_hosteddomain ) > 0
887 ) {
888 // Allow multiple whitelisted domains.
889 $oauth2_hosteddomains = explode( "\n", str_replace( "\r", '', $oauth2_hosteddomain ) );
890 $valid_domain = false;
891 foreach ( $externally_authenticated_email as $email ) {
892 $email_domain = substr( strrchr( $email, '@' ), 1 );
893 if ( in_array( $email_domain, $oauth2_hosteddomains, true ) ) {
894 $valid_domain = true;
895 }
896 }
897 if ( ! $valid_domain ) {
898 $this->custom_logout();
899 return new \WP_Error( 'invalid_oauth2_login', __( 'Email address does not match the allowed hosted domain', 'authorizer' ) );
900 }
901 }
902
903 // Get user first name (handle string or array results from attribute).
904 $first_name = '';
905 $oauth2_attr_first_name = $oauth2_attr_first_name ?? '';
906 if ( ! empty( $oauth2_attr_first_name ) && ! empty( $attributes[ $oauth2_attr_first_name ] ) ) {
907 if ( is_string( $attributes[ $oauth2_attr_first_name ] ) ) {
908 $first_name = $attributes[ $oauth2_attr_first_name ];
909 } elseif ( is_array( $attributes[ $oauth2_attr_first_name ] ) ) {
910 $first_name = trim( implode( ' ', $attributes[ $oauth2_attr_first_name ] ) );
911 }
912 }
913
914 // Get user last name (handle string or array results from attribute).
915 $last_name = '';
916 $oauth2_attr_last_name = $oauth2_attr_last_name ?? '';
917 if ( ! empty( $oauth2_attr_last_name ) && ! empty( $attributes[ $oauth2_attr_last_name ] ) ) {
918 if ( is_string( $attributes[ $oauth2_attr_last_name ] ) ) {
919 $last_name = $attributes[ $oauth2_attr_last_name ];
920 } elseif ( is_array( $attributes[ $oauth2_attr_last_name ] ) ) {
921 $last_name = trim( implode( ' ', $attributes[ $oauth2_attr_last_name ] ) );
922 }
923 }
924
925 return array(
926 'email' => $externally_authenticated_email,
927 'username' => sanitize_user( $username ),
928 'first_name' => $first_name,
929 'last_name' => $last_name,
930 'authenticated_by' => 'oauth2',
931 'oauth2_provider' => $oauth2_provider,
932 'oauth2_attributes' => $attributes,
933 'oauth2_server_id' => $oauth2_server_id,
934 );
935 }
936
937 /**
938 * Validate this user's credentials against OIDC provider.
939 *
940 * @param array $auth_settings Plugin settings.
941 * @return array|WP_Error Array containing email, authenticated_by, first_name,
942 * last_name, and username strings for the successfully
943 * authenticated user, or WP_Error() object on failure,
944 * or null if not attempting an OIDC login.
945 */
946 protected function custom_authenticate_oidc( $auth_settings ) {
947 // Move on if oidc hasn't been requested here or OIDC server ID is invalid.
948 if ( empty( $auth_settings['oidc_num_servers'] ) ) {
949 $auth_settings['oidc_num_servers'] = 1;
950 }
951
952 // Workaround: because some OIDC providers don't let us specify a querystring in a
953 // redirect_uri, we have to detect those redirects separately because we
954 // can't include external=oidc or id={oidc_server_id} in the URL.
955 // Instead, detect the absence of the `external` param, and the presence of
956 // `code` and `state` params (OIDC uses authorization code flow).
957 if ( empty( $_GET['external'] ) && ! empty( $_GET['code'] ) && ! empty( $_GET['state'] ) ) {
958 // Fetch the OIDC server id from the session variable created during the
959 // initial request.
960 if ( PHP_SESSION_NONE === session_status() ) {
961 session_start();
962 }
963 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
964 $_GET['id'] = $_SESSION['oidc_server_id'] ?? 1;
965 $_GET['external'] = 'oidc';
966 }
967
968 // If this is an OIDC login attempt and the id param is missing, default
969 // it to 1.
970 if ( ! empty( $_GET['external'] ) && 'oidc' === $_GET['external'] && empty( $_GET['id'] ) ) {
971 $_GET['id'] = 1;
972 }
973
974 // phpcs:ignore WordPress.Security.NonceVerification
975 if ( empty( $_GET['external'] ) || 'oidc' !== $_GET['external'] || empty( $_GET['id'] ) || ! in_array( intval( $_GET['id'] ), range( 1, 20 ), true ) || intval( $_GET['id'] ) > intval( $auth_settings['oidc_num_servers'] ) ) {
976 return null;
977 }
978
979 // Get the OIDC server id (since multiple OIDC servers can be configured),
980 // and the relevant settings for that server.
981 // phpcs:ignore WordPress.Security.NonceVerification
982 $oidc_server_id = empty( $_GET['id'] ) ? 1 : intval( $_GET['id'] );
983 $suffix = $oidc_server_id > 1 ? '_' . $oidc_server_id : '';
984 $oidc_issuer = $auth_settings[ 'oidc_issuer' . $suffix ] ?? '';
985 $oidc_client_id = $auth_settings[ 'oidc_client_id' . $suffix ] ?? '';
986 $oidc_client_secret = $auth_settings[ 'oidc_client_secret' . $suffix ] ?? '';
987 $oidc_scopes = $auth_settings[ 'oidc_scopes' . $suffix ] ?? 'openid email profile';
988 $oidc_prompt = $auth_settings[ 'oidc_prompt' . $suffix ] ?? '';
989 $oidc_login_hint = $auth_settings[ 'oidc_login_hint' . $suffix ] ?? '';
990 $oidc_max_age = $auth_settings[ 'oidc_max_age' . $suffix ] ?? '';
991 $oidc_attr_username = $auth_settings[ 'oidc_attr_username' . $suffix ] ?? 'preferred_username';
992 $oidc_attr_email = $auth_settings[ 'oidc_attr_email' . $suffix ] ?? 'email';
993 $oidc_attr_first_name = $auth_settings[ 'oidc_attr_first_name' . $suffix ] ?? 'given_name';
994 $oidc_attr_last_name = $auth_settings[ 'oidc_attr_last_name' . $suffix ] ?? 'family_name';
995 $oidc_require_verified_email = $auth_settings[ 'oidc_require_verified_email' . $suffix ] ?? '';
996 $oidc_link_on_username = $auth_settings[ 'oidc_link_on_username' . $suffix ] ?? '';
997 $oidc_hosteddomain = $auth_settings[ 'oidc_hosteddomain' . $suffix ] ?? '';
998
999 // Fetch the OIDC Client ID (allow overrides from filter or constant).
1000 // Note: constant/filter overrides are only supported for a single OIDC server.
1001 if ( 1 === $oidc_server_id && defined( 'AUTHORIZER_OIDC_CLIENT_ID' ) ) {
1002 $oidc_client_id = \AUTHORIZER_OIDC_CLIENT_ID;
1003 }
1004 /**
1005 * Filters the OIDC Client ID used by Authorizer to authenticate.
1006 *
1007 * @since 3.11.0
1008 *
1009 * @param string $oidc_client_id The stored OIDC Client ID.
1010 */
1011 if ( 1 === $oidc_server_id ) {
1012 $oidc_client_id = apply_filters( 'authorizer_oidc_client_id', $oidc_client_id );
1013 }
1014
1015 // Fetch the OIDC Client Secret (allow overrides from filter or constant).
1016 // Note: constant/filter overrides are only supported for a single OIDC server.
1017 if ( 1 === $oidc_server_id && defined( 'AUTHORIZER_OIDC_CLIENT_SECRET' ) ) {
1018 $oidc_client_secret = \AUTHORIZER_OIDC_CLIENT_SECRET;
1019 }
1020 /**
1021 * Filters the OIDC Client Secret used by Authorizer to authenticate.
1022 *
1023 * @since 3.11.0
1024 *
1025 * @param string $oidc_client_secret The stored OIDC Client Secret.
1026 */
1027 if ( 1 === $oidc_server_id ) {
1028 $oidc_client_secret = apply_filters( 'authorizer_oidc_client_secret', $oidc_client_secret );
1029 }
1030
1031 // Move on if required params aren't specified in settings.
1032 if (
1033 empty( $oidc_issuer ) ||
1034 empty( $oidc_client_id ) ||
1035 empty( $oidc_client_secret )
1036 ) {
1037 return null;
1038 }
1039
1040 // Build the redirectUri for the OIDC provider to redirect back to.
1041 // Note: omit the id param if it is 1 (default server) for consistency with
1042 // the CAS and OAuth2 implementations.
1043 $redirect_uri = site_url( '/wp-login.php?external=oidc' );
1044 if ( $oidc_server_id > 1 ) {
1045 $redirect_uri .= '&id=' . $oidc_server_id;
1046 }
1047
1048 // Start session for state/nonce/PKCE storage.
1049 if ( PHP_SESSION_NONE === session_status() ) {
1050 session_start();
1051 }
1052
1053 // Save redirect_to parameter if present.
1054 if ( ! empty( $_GET['redirect_to'] ) ) {
1055 // phpcs:ignore WordPress.Security.NonceVerification
1056 $_SESSION['oidc_redirect_to'] = sanitize_url( wp_unslash( $_GET['redirect_to'] ) );
1057 }
1058
1059 // Initialize jumbojett OIDC client.
1060 try {
1061 $oidc = new \Jumbojett\OpenIDConnectClient(
1062 $oidc_issuer,
1063 $oidc_client_id,
1064 $oidc_client_secret
1065 );
1066
1067 // Set redirect URL.
1068 $oidc->setRedirectURL( $redirect_uri );
1069
1070 // Save the OIDC server id so we can restore it after a successful
1071 // login (note: we can't add the id querystring param to the redirectUri
1072 // param above because some providers won't match the approved URI set in their portal).
1073 $_SESSION['oidc_server_id'] = $oidc_server_id;
1074
1075 // Enable PKCE with S256.
1076 $oidc->setCodeChallengeMethod( 'S256' );
1077
1078 // Add scopes.
1079 $scopes = array_filter( array_map( 'trim', explode( ' ', $oidc_scopes ) ) );
1080 if ( empty( $scopes ) ) {
1081 $scopes = array( 'openid', 'email', 'profile' );
1082 }
1083 $oidc->addScope( $scopes );
1084
1085 // Add optional parameters.
1086 if ( ! empty( $oidc_prompt ) ) {
1087 $oidc->addAuthParam( array( 'prompt' => $oidc_prompt ) );
1088 }
1089 if ( ! empty( $oidc_login_hint ) ) {
1090 $oidc->addAuthParam( array( 'login_hint' => $oidc_login_hint ) );
1091 }
1092 if ( ! empty( $oidc_max_age ) ) {
1093 $oidc->addAuthParam( array( 'max_age' => $oidc_max_age ) );
1094 }
1095
1096 // Authenticate (library handles PKCE, nonce, state).
1097 $oidc->authenticate();
1098
1099 // Get ID token for RP-initiated logout (will be stored in user meta after user is found).
1100 $id_token = $oidc->getIdToken();
1101
1102 // Get user info from userinfo endpoint (if available).
1103 // Convert stdClass object to array to match codebase pattern (like OAuth2).
1104 $user_info = array();
1105 try {
1106 $user_info = (array) $oidc->requestUserInfo();
1107 } catch ( \Exception $e ) {
1108 // Userinfo endpoint may not be available or may fail, continue with ID token.
1109 }
1110
1111 // Also get ID token payload (email is often in ID token).
1112 $id_token_payload = array();
1113 try {
1114 $id_token_payload_obj = $oidc->getIdTokenPayload();
1115 if ( $id_token_payload_obj ) {
1116 $id_token_payload = (array) $id_token_payload_obj;
1117 }
1118 } catch ( \Exception $e ) {
1119 // ID token payload unavailable.
1120 }
1121
1122 // Merge ID token claims with userinfo (userinfo takes precedence).
1123 $user_info = array_merge( $id_token_payload, $user_info );
1124
1125 // Extract email.
1126 $email = '';
1127 if ( ! empty( $oidc_attr_email ) && ! empty( $user_info[ $oidc_attr_email ] ) ) {
1128 $email = Helper::lowercase( sanitize_email( $user_info[ $oidc_attr_email ] ) );
1129 } elseif ( ! empty( $user_info['email'] ) ) {
1130 $email = Helper::lowercase( sanitize_email( $user_info['email'] ) );
1131 }
1132
1133 // Extract username.
1134 $username = '';
1135 if ( ! empty( $oidc_attr_username ) && ! empty( $user_info[ $oidc_attr_username ] ) ) {
1136 $username = sanitize_user( $user_info[ $oidc_attr_username ] );
1137 } elseif ( ! empty( $user_info['preferred_username'] ) ) {
1138 $username = sanitize_user( $user_info['preferred_username'] );
1139 } elseif ( ! empty( $user_info['sub'] ) ) {
1140 $username = sanitize_user( $user_info['sub'] );
1141 }
1142
1143 // If linking by username is enabled, email is optional.
1144 // Otherwise, email is required.
1145 if ( '1' !== $oidc_link_on_username ) {
1146 if ( empty( $email ) ) {
1147 // Clean up session variables before returning error.
1148 \Authorizer\Options\External\Oidc::get_instance()->maybe_unset_oidc_session_vars();
1149 return new \WP_Error( 'oidc_no_email', __( '<strong>ERROR</strong>: OIDC provider did not return an email address.', 'authorizer' ) );
1150 }
1151
1152 // Enforce email verification if required.
1153 if ( '1' === $oidc_require_verified_email ) {
1154 if ( empty( $user_info['email_verified'] ) || true !== $user_info['email_verified'] ) {
1155 // Clean up session variables before returning error.
1156 \Authorizer\Options\External\Oidc::get_instance()->maybe_unset_oidc_session_vars();
1157 return new \WP_Error( 'oidc_email_not_verified', __( '<strong>ERROR</strong>: Email address must be verified to log in.', 'authorizer' ) );
1158 }
1159 }
1160
1161 // Enforce hosted domain allowlist if configured.
1162 if ( ! empty( $oidc_hosteddomain ) ) {
1163 $allowed_domains = array_filter( array_map( 'trim', explode( "\n", str_replace( "\r", '', $oidc_hosteddomain ) ) ) );
1164 $email_domain = substr( strrchr( $email, '@' ), 1 );
1165 if ( ! in_array( $email_domain, $allowed_domains, true ) ) {
1166 // Clean up session variables before returning error.
1167 \Authorizer\Options\External\Oidc::get_instance()->maybe_unset_oidc_session_vars();
1168 return new \WP_Error( 'oidc_domain_not_allowed', __( '<strong>ERROR</strong>: Your email domain is not allowed to log in.', 'authorizer' ) );
1169 }
1170 }
1171
1172 // Fallback username to email username part if username is empty.
1173 if ( empty( $username ) ) {
1174 $username = sanitize_user( substr( $email, 0, strpos( $email, '@' ) ) );
1175 }
1176 } elseif ( empty( $username ) ) {
1177 // When linking by username, username is required.
1178 // Clean up session variables before returning error.
1179 \Authorizer\Options\External\Oidc::get_instance()->maybe_unset_oidc_session_vars();
1180 return new \WP_Error( 'oidc_no_username', __( '<strong>ERROR</strong>: OIDC provider did not return a username.', 'authorizer' ) );
1181 }
1182
1183 // Extract first name.
1184 $first_name = '';
1185 if ( ! empty( $oidc_attr_first_name ) && ! empty( $user_info[ $oidc_attr_first_name ] ) ) {
1186 $first_name = sanitize_text_field( $user_info[ $oidc_attr_first_name ] );
1187 } elseif ( ! empty( $user_info['given_name'] ) ) {
1188 $first_name = sanitize_text_field( $user_info['given_name'] );
1189 }
1190
1191 // Extract last name.
1192 $last_name = '';
1193 if ( ! empty( $oidc_attr_last_name ) && ! empty( $user_info[ $oidc_attr_last_name ] ) ) {
1194 $last_name = sanitize_text_field( $user_info[ $oidc_attr_last_name ] );
1195 } elseif ( ! empty( $user_info['family_name'] ) ) {
1196 $last_name = sanitize_text_field( $user_info['family_name'] );
1197 }
1198
1199 return array(
1200 'email' => $email,
1201 'username' => $username,
1202 'first_name' => $first_name,
1203 'last_name' => $last_name,
1204 'authenticated_by' => 'oidc',
1205 'oidc_attributes' => $user_info,
1206 'oidc_server_id' => $oidc_server_id,
1207 'oidc_id_token' => $id_token,
1208 );
1209 } catch ( \Exception $e ) {
1210 // Clean up session variables on exception.
1211 \Authorizer\Options\External\Oidc::get_instance()->maybe_unset_oidc_session_vars();
1212
1213 // Log the error to error_log.
1214 error_log( __( 'OIDC authentication failed. Details:', 'authorizer' ) ); // phpcs:ignore
1215 error_log( $e->getMessage() ); // phpcs:ignore
1216
1217 // Also log the error to the Simple History plugin (if it is active).
1218 apply_filters(
1219 'simple_history_log_warning',
1220 __( 'OIDC authentication failed. Details:', 'authorizer' ),
1221 array(
1222 'error' => $e->getMessage(),
1223 )
1224 );
1225
1226 return new \WP_Error( 'oidc_error', __( '<strong>ERROR</strong>: OIDC authentication failed.', 'authorizer' ) . ' ' . esc_html( $e->getMessage() ) );
1227 }
1228 }
1229
1230 /**
1231 * Validate this user's credentials against Google.
1232 *
1233 * @param array $auth_settings Plugin settings.
1234 * @return array|WP_Error Array containing email, authenticated_by, first_name,
1235 * last_name, and username strings for the successfully
1236 * authenticated user, or WP_Error() object on failure,
1237 * or null if not attempting a google login.
1238 */
1239 protected function custom_authenticate_google( $auth_settings ) {
1240 // Move on if Google auth hasn't been requested here.
1241 // phpcs:ignore WordPress.Security.NonceVerification
1242 if ( empty( $_GET['external'] ) || 'google' !== $_GET['external'] ) {
1243 return null;
1244 }
1245
1246 // Get one time use token.
1247 if ( PHP_SESSION_NONE === session_status() ) {
1248 session_start();
1249 }
1250 $token = array_key_exists( 'token', $_SESSION ) ? $_SESSION['token'] : null;
1251
1252 // No token, so this is not a succesful Google login.
1253 if ( empty( $token ) ) {
1254 return null;
1255 }
1256
1257 // Fetch the Google Client ID (allow overrides from filter or constant).
1258 if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_ID' ) ) {
1259 $auth_settings['google_clientid'] = \AUTHORIZER_GOOGLE_CLIENT_ID;
1260 }
1261 /**
1262 * Filters the Google Client ID used by Authorizer to authenticate.
1263 *
1264 * @since 3.9.0
1265 *
1266 * @param string $google_client_id The stored Google Client ID.
1267 */
1268 $auth_settings['google_clientid'] = apply_filters( 'authorizer_google_client_id', $auth_settings['google_clientid'] );
1269
1270 // Fetch the Google Client Secret (allow overrides from filter or constant).
1271 if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_SECRET' ) ) {
1272 $auth_settings['google_clientsecret'] = \AUTHORIZER_GOOGLE_CLIENT_SECRET;
1273 }
1274 /**
1275 * Filters the Google Client Secret used by Authorizer to authenticate.
1276 *
1277 * @since 3.6.1
1278 *
1279 * @param string $google_client_secret The stored Google Client Secret.
1280 */
1281 $auth_settings['google_clientsecret'] = apply_filters( 'authorizer_google_client_secret', $auth_settings['google_clientsecret'] );
1282
1283 // Build the Google Client.
1284 $client = new \Google_Client();
1285 $client->setApplicationName( 'WordPress' );
1286 $client->setClientId( trim( $auth_settings['google_clientid'] ) );
1287 $client->setClientSecret( trim( $auth_settings['google_clientsecret'] ) );
1288 $client->setRedirectUri( 'postmessage' );
1289
1290 /**
1291 * If the hosted domain parameter is set, restrict logins to that domain
1292 * (only available in google-api-php-client v2 or higher).
1293 */
1294 if (
1295 array_key_exists( 'google_hosteddomain', $auth_settings ) &&
1296 strlen( $auth_settings['google_hosteddomain'] ) > 0 &&
1297 $client::LIBVER >= '2.0.0'
1298 ) {
1299 $google_hosteddomains = explode( "\n", str_replace( "\r", '', $auth_settings['google_hosteddomain'] ) );
1300 $google_hosteddomain = trim( $google_hosteddomains[0] );
1301 $client->setHostedDomain( $google_hosteddomain );
1302 }
1303
1304 // Allow minor clock drift between this server's clock and Google's.
1305 // See: https://github.com/googleapis/google-api-php-client/issues/1630
1306 \Firebase\JWT\JWT::$leeway = 30;
1307
1308 // Verify this is a successful Google authentication.
1309 try {
1310 $payload = $client->verifyIdToken( $token );
1311 } catch ( \Firebase\JWT\BeforeValidException $e ) {
1312 // Server clock out of sync with Google servers.
1313 return new \WP_Error( 'invalid_google_login', __( 'The authentication timestamp is too old, please try again.', 'authorizer' ) );
1314 } catch ( Google_Auth_Exception $e ) {
1315 // Invalid ticket, so this in not a successful Google login.
1316 return new \WP_Error( 'invalid_google_login', __( 'Invalid Google credentials provided.', 'authorizer' ) );
1317 }
1318
1319 // Invalid ticket, so this in not a successful Google login.
1320 if ( empty( $payload['email'] ) ) {
1321 return new \WP_Error( 'invalid_google_login', __( 'Invalid Google credentials provided.', 'authorizer' ) );
1322 }
1323
1324 // Get email address.
1325 $email = Helper::lowercase( $payload['email'] );
1326
1327 $email_domain = substr( strrchr( $email, '@' ), 1 );
1328 $username = current( explode( '@', $email ) );
1329
1330 /**
1331 * Fail if hd param is set and the logging in user's email address doesn't
1332 * match the allowed hosted domain.
1333 *
1334 * See: https://developers.google.com/identity/protocols/OpenIDConnect#hd-param
1335 * See: https://github.com/google/google-api-php-client/blob/v1-master/src/Google/Client.php#L407-L416
1336 *
1337 * Note: this is a failsafe if the setHostedDomain() feature in v2 does not work above.
1338 */
1339 if (
1340 array_key_exists( 'google_hosteddomain', $auth_settings ) &&
1341 strlen( $auth_settings['google_hosteddomain'] ) > 0
1342 ) {
1343 // Allow multiple whitelisted domains.
1344 $google_hosteddomains = explode( "\n", str_replace( "\r", '', $auth_settings['google_hosteddomain'] ) );
1345 if ( ! in_array( $email_domain, $google_hosteddomains, true ) ) {
1346 $this->custom_logout();
1347 return new \WP_Error( 'invalid_google_login', __( 'Google credentials do not match the allowed hosted domain', 'authorizer' ) );
1348 }
1349 }
1350
1351 return array(
1352 'email' => $email,
1353 'username' => $username,
1354 'first_name' => '',
1355 'last_name' => '',
1356 'authenticated_by' => 'google',
1357 'google_attributes' => $payload,
1358 );
1359 }
1360
1361
1362 /**
1363 * Validate this user's credentials against CAS.
1364 *
1365 * @param array $auth_settings Plugin settings.
1366 * @return array|WP_Error Array containing 'email' and 'authenticated_by' strings
1367 * for the successfully authenticated user, or WP_Error()
1368 * object on failure, or null if not attempting a CAS login.
1369 */
1370 protected function custom_authenticate_cas( $auth_settings ) {
1371 // Move on if CAS hasn't been requested here or the CAS server ID is invalid.
1372 if ( empty( $auth_settings['cas_num_servers'] ) ) {
1373 $auth_settings['cas_num_servers'] = 1;
1374 }
1375 // phpcs:ignore WordPress.Security.NonceVerification
1376 if ( empty( $_GET['external'] ) || 'cas' !== $_GET['external'] || empty( $_GET['id'] ) || ! in_array( intval( $_GET['id'] ), range( 1, 10 ), true ) || intval( $_GET['id'] ) > intval( $auth_settings['cas_num_servers'] ) ) {
1377 return null;
1378 }
1379 // Get the CAS server id (since multiple CAS servers can be configured), and
1380 // the relevant CAS settings for that server.
1381 // phpcs:ignore WordPress.Security.NonceVerification
1382 $cas_server_id = intval( $_GET['id'] );
1383 $suffix = $cas_server_id > 1 ? '_' . $cas_server_id : '';
1384 $cas_host = $auth_settings[ 'cas_host' . $suffix ];
1385 $cas_port = $auth_settings[ 'cas_port' . $suffix ];
1386 $cas_path = $auth_settings[ 'cas_path' . $suffix ];
1387 $cas_method = $auth_settings[ 'cas_method' . $suffix ];
1388 $cas_version = $auth_settings[ 'cas_version' . $suffix ];
1389 $cas_attr_email = $auth_settings[ 'cas_attr_email' . $suffix ];
1390 $cas_attr_first_name = $auth_settings[ 'cas_attr_first_name' . $suffix ];
1391 $cas_attr_last_name = $auth_settings[ 'cas_attr_last_name' . $suffix ];
1392
1393 /**
1394 * Get the CAS server protocol version (default to SAML 1.1).
1395 *
1396 * @see: https://apereo.github.io/phpCAS/api/group__public.html#gadea9415f40b8d2afc39f140c9be83bbe
1397 */
1398 $cas_version = Options\External\Cas::get_instance()->sanitize_cas_version( $cas_version );
1399
1400 /**
1401 * Get valid service URLs for the CAS client to validate against.
1402 *
1403 * @see: https://github.com/apereo/phpCAS/security/advisories/GHSA-8q72-6qq8-xv64
1404 */
1405 $valid_base_urls = Options\External\Cas::get_instance()->get_valid_cas_service_urls();
1406
1407 // Set the CAS client configuration.
1408 if ( 'PROXY' === strtoupper( $cas_method ) ) {
1409 \phpCAS::proxy( $cas_version, $cas_host, intval( $cas_port ), $cas_path, $valid_base_urls );
1410 } else {
1411 \phpCAS::client( $cas_version, $cas_host, intval( $cas_port ), $cas_path, $valid_base_urls );
1412 }
1413
1414 // Allow redirects at the CAS server endpoint (e.g., allow connections
1415 // at an old CAS URL that redirects to a newer CAS URL).
1416 \phpCAS::setExtraCurlOption( CURLOPT_FOLLOWLOCATION, true );
1417
1418 // Use the WordPress certificate bundle at /wp-includes/certificates/ca-bundle.crt.
1419 \phpCAS::setCasServerCACert( ABSPATH . WPINC . '/certificates/ca-bundle.crt' );
1420
1421 // Set the CAS service URL (including the redirect URL for WordPress when it comes back from CAS).
1422 $cas_service_url = site_url( '/wp-login.php?external=cas&id=' . $cas_server_id );
1423 $login_querystring = array();
1424 if ( isset( $_SERVER['QUERY_STRING'] ) ) {
1425 parse_str( $_SERVER['QUERY_STRING'], $login_querystring ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
1426 }
1427 if ( isset( $login_querystring['redirect_to'] ) ) {
1428 $cas_service_url .= '&redirect_to=' . rawurlencode( $login_querystring['redirect_to'] );
1429 }
1430 \phpCAS::setFixedServiceURL( $cas_service_url );
1431
1432 // Authenticate against CAS.
1433 try {
1434 // phpcs:ignore Squiz.PHP.CommentedOutCode
1435 // \phpCAS::setDebug( dirname( __FILE__ ) . '/../../debug.log' );
1436 \phpCAS::forceAuthentication();
1437 } catch ( \CAS_AuthenticationException $e ) {
1438 // CAS server threw an error in isAuthenticated(), potentially because
1439 // the cached ticket is outdated. Try renewing the authentication.
1440 error_log( __( 'CAS server returned an Authentication Exception. Details:', 'authorizer' ) ); // phpcs:ignore
1441 error_log( $e->getMessage() ); // phpcs:ignore
1442
1443 // Also log the error to the Simple History plugin (if it is active).
1444 apply_filters(
1445 'simple_history_log_warning',
1446 __( 'CAS server returned an Authentication Exception. Details:', 'authorizer' ),
1447 array(
1448 'error' => $e->getMessage(),
1449 )
1450 );
1451
1452 // CAS server is throwing errors on this login, so try logging the
1453 // user out of CAS and redirecting them to the login page.
1454 \phpCAS::logoutWithRedirectService( wp_login_url() );
1455 die();
1456 }
1457
1458 // Get username (as specified by the CAS server).
1459 $username = \phpCAS::getUser();
1460
1461 // Get email that successfully authenticated against the external service (CAS).
1462 $externally_authenticated_email = strtolower( $username );
1463 if ( ! filter_var( $externally_authenticated_email, FILTER_VALIDATE_EMAIL ) ) {
1464 // If we can't get the user's email address from a CAS attribute,
1465 // try to guess the domain from the CAS server hostname. This will only
1466 // be used if we can't discover the email address from CAS attributes.
1467 $domain_guess = preg_match( '/[^.]*\.[^.]*$/', $cas_host, $matches ) === 1 ? $matches[0] : '';
1468 $externally_authenticated_email = Helper::lowercase( $username ) . '@' . $domain_guess;
1469 }
1470
1471 // Retrieve the user attributes (e.g., email address, first name, last name) from the CAS server.
1472 $cas_attributes = \phpCAS::getAttributes();
1473
1474 // Get user email if it is specified in another field.
1475 if ( ! empty( $cas_attr_email ) ) {
1476 // If the email attribute starts with an at symbol (@), assume that the
1477 // email domain is manually entered there (instead of a reference to a
1478 // CAS attribute), and combine that with the username to create the email.
1479 // Otherwise, look up the CAS attribute for email.
1480 if ( substr( $cas_attr_email, 0, 1 ) === '@' ) {
1481 $externally_authenticated_email = Helper::lowercase( $username . $cas_attr_email );
1482 } elseif (
1483 // If a CAS attribute has been specified as containing the email address, use that instead.
1484 // Email attribute can be a string or an array of strings.
1485 array_key_exists( $cas_attr_email, $cas_attributes ) && (
1486 (
1487 is_array( $cas_attributes[ $cas_attr_email ] ) &&
1488 count( $cas_attributes[ $cas_attr_email ] ) > 0
1489 ) || (
1490 is_string( $cas_attributes[ $cas_attr_email ] ) &&
1491 strlen( $cas_attributes[ $cas_attr_email ] ) > 0
1492 )
1493 )
1494 ) {
1495 // Each of the emails in the array needs to be set to lowercase.
1496 if ( is_array( $cas_attributes[ $cas_attr_email ] ) ) {
1497 $externally_authenticated_email = array();
1498 foreach ( $cas_attributes[ $cas_attr_email ] as $external_email ) {
1499 $externally_authenticated_email[] = Helper::lowercase( $external_email );
1500 }
1501 } else {
1502 $externally_authenticated_email = Helper::lowercase( $cas_attributes[ $cas_attr_email ] );
1503 }
1504 }
1505 }
1506
1507 // Get user first name (handle string or array results from CAS attribute).
1508 $first_name = '';
1509 if ( ! empty( $cas_attr_first_name ) && ! empty( $cas_attributes[ $cas_attr_first_name ] ) ) {
1510 if ( is_string( $cas_attributes[ $cas_attr_first_name ] ) ) {
1511 $first_name = $cas_attributes[ $cas_attr_first_name ];
1512 } elseif ( is_array( $cas_attributes[ $cas_attr_first_name ] ) ) {
1513 $first_name = trim( implode( ' ', $cas_attributes[ $cas_attr_first_name ] ) );
1514 }
1515 }
1516
1517 // Get user last name (handle string or array results from CAS attribute).
1518 $last_name = '';
1519 if ( ! empty( $cas_attr_last_name ) && ! empty( $cas_attributes[ $cas_attr_last_name ] ) ) {
1520 if ( is_string( $cas_attributes[ $cas_attr_last_name ] ) ) {
1521 $last_name = $cas_attributes[ $cas_attr_last_name ];
1522 } elseif ( is_array( $cas_attributes[ $cas_attr_last_name ] ) ) {
1523 $last_name = trim( implode( ' ', $cas_attributes[ $cas_attr_last_name ] ) );
1524 }
1525 }
1526
1527 return array(
1528 'email' => $externally_authenticated_email,
1529 'username' => $username,
1530 'first_name' => $first_name,
1531 'last_name' => $last_name,
1532 'authenticated_by' => 'cas',
1533 'cas_attributes' => $cas_attributes,
1534 'cas_server_id' => $cas_server_id,
1535 );
1536 }
1537
1538
1539 /**
1540 * Validate this user's credentials against LDAP.
1541 *
1542 * @param array $auth_settings Plugin settings.
1543 * @param string $username Attempted username from authenticate action.
1544 * @param string $password Attempted password from authenticate action.
1545 * @param array $debug If provided, filled with an array of debug
1546 * messages. Defaults to null.
1547 *
1548 * @return array|WP_Error Array containing 'email' and 'authenticated_by' strings
1549 * for the successfully authenticated user, or WP_Error()
1550 * object on failure, or null if skipping LDAP auth and
1551 * falling back to WP auth.
1552 */
1553 public function custom_authenticate_ldap( $auth_settings, $username, $password, &$debug = null ) {
1554 // Make sure all LDAP settings are defined (user and password can be
1555 // overridden by constant or filter and may not exist in auth_settings).
1556 $defaults = array(
1557 'ldap' => '',
1558 'ldap_host' => '',
1559 'ldap_port' => '389',
1560 'ldap_tls' => '1',
1561 'ldap_search_base' => '',
1562 'ldap_search_filter' => '',
1563 'ldap_uid' => 'uid',
1564 'ldap_attr_email' => '',
1565 'ldap_user' => '',
1566 'ldap_password' => '',
1567 'ldap_lostpassword_url' => '',
1568 'ldap_attr_first_name' => '',
1569 'ldap_attr_last_name' => '',
1570 'ldap_attr_update_on_login' => '',
1571 'ldap_test_user' => '',
1572 );
1573 $auth_settings = wp_parse_args( $auth_settings, $defaults );
1574
1575 // Initialize debug array if a variable was passed in.
1576 if ( ! is_null( $debug ) ) {
1577 $debug = array(
1578 /* TRANSLATORS: Current time */
1579 sprintf( __( '[%s] Attempting to authenticate via LDAP.', 'authorizer' ), wp_date( get_option( 'time_format' ) ) ),
1580 );
1581 }
1582
1583 // Get LDAP host(s), and attempt each until we have a valid connection.
1584 $ldap_hosts = explode( "\n", str_replace( "\r", '', trim( $auth_settings['ldap_host'] ) ) );
1585
1586 // Fail silently (fall back to WordPress authentication) if no LDAP host specified.
1587 if ( count( $ldap_hosts ) < 1 ) {
1588 if ( is_array( $debug ) ) {
1589 $debug[] = __( 'Failed: no LDAP Host(s) specified.', 'authorizer' );
1590 }
1591 return null;
1592 }
1593
1594 // Get LDAP search base(s).
1595 $search_bases = explode( "\n", str_replace( "\r", '', trim( $auth_settings['ldap_search_base'] ) ) );
1596
1597 // Fail silently (fall back to WordPress authentication) if no search base specified.
1598 if ( count( $search_bases ) < 1 ) {
1599 if ( is_array( $debug ) ) {
1600 $debug[] = __( 'Failed: no LDAP Search Base(s) specified.', 'authorizer' );
1601 }
1602 return null;
1603 }
1604
1605 // Get the FQDN from the first LDAP search base domain components (dc). For
1606 // example, ou=people,dc=example,dc=edu,dc=uk would yield user@example.edu.uk.
1607 $search_base_components = explode( ',', trim( $search_bases[0] ) );
1608 $domain = array();
1609 foreach ( $search_base_components as $search_base_component ) {
1610 $component = explode( '=', $search_base_component );
1611 if ( 2 === count( $component ) && 'dc' === $component[0] ) {
1612 $domain[] = $component[1];
1613 }
1614 }
1615 $domain = implode( '.', $domain );
1616
1617 // If we can't get the logging in user's email address from an LDAP attribute,
1618 // just use the domain from the LDAP host. This will only be used if we
1619 // can't discover the email address from an LDAP attribute.
1620 if ( empty( $domain ) ) {
1621 $domain = preg_match( '/[^.]*\.[^.]*$/', $ldap_hosts[0], $matches ) === 1 ? $matches[0] : '';
1622 }
1623
1624 // remove @domain if it exists in the username (i.e., if user entered their email).
1625 $username = str_replace( '@' . $domain, '', $username );
1626
1627 // Fail silently (fall back to WordPress authentication) if both username
1628 // and password are empty (this will be the case when visiting wp-login.php
1629 // for the first time, or when clicking the Log In button without filling
1630 // out either field.
1631 if ( empty( $username ) && empty( $password ) ) {
1632 if ( is_array( $debug ) ) {
1633 $debug[] = __( 'Failed: empty username and password.', 'authorizer' );
1634 }
1635 return null;
1636 }
1637
1638 // Fail with error message if username or password is blank.
1639 if ( empty( $username ) ) {
1640 if ( is_array( $debug ) ) {
1641 $debug[] = __( 'Failed: empty username.', 'authorizer' );
1642 }
1643 return new \WP_Error( 'empty_username', __( 'You must provide a username or email.', 'authorizer' ) );
1644 }
1645 if ( empty( $password ) ) {
1646 if ( is_array( $debug ) ) {
1647 $debug[] = __( 'Failed: empty password.', 'authorizer' );
1648 }
1649 return new \WP_Error( 'empty_password', __( 'You must provide a password.', 'authorizer' ) );
1650 }
1651
1652 // If php5-ldap extension isn't installed on server, fall back to WP auth.
1653 if ( ! function_exists( 'ldap_connect' ) ) {
1654 if ( is_array( $debug ) ) {
1655 $debug[] = __( 'Failed: php-ldap extension not installed.', 'authorizer' );
1656 }
1657 return null;
1658 }
1659
1660 // Authenticate against LDAP using options provided in plugin settings.
1661 $result = false;
1662 $ldap_user_dn = '';
1663 $first_name = '';
1664 $last_name = '';
1665 $email = '';
1666
1667 // Attempt each LDAP host until we have a valid connection.
1668 $ldap_valid = false;
1669 foreach ( $ldap_hosts as $ldap_host ) {
1670 // Construct LDAP connection parameters. In PHP < 8.3, ldap_connect()
1671 // takes either a hostname or a full LDAP URI as its first parameter
1672 // (works with OpenLDAP 2.x.x or later). If it's an LDAP URI, the second
1673 // parameter, $port, is ignored, and port must be specified in the full
1674 // URI. An LDAP URI is of the form ldap://hostname:port or
1675 // ldaps://hostname:port.
1676 // In PHP 8.3, ldap_connect() only takes a single param (the signature
1677 // with 2 params is deprecated). We thus convert all LDAP hosts to a full
1678 // LDAP URI, defaulting to ldap:// if the full URI isn't provided.
1679 $ldap_port = intval( $auth_settings['ldap_port'] );
1680 $parsed_host = wp_parse_url( $ldap_host );
1681
1682 // Fail if invalid host is specified.
1683 if ( false === $parsed_host ) {
1684 if ( is_array( $debug ) ) {
1685 /* TRANSLATORS: LDAP Host */
1686 $debug[] = sprintf( __( 'Warning: could not parse host %s with wp_parse_url().', 'authorizer' ), $ldap_host );
1687 }
1688 continue;
1689 }
1690
1691 // If a scheme is in the LDAP host, use full LDAP URI instead of just hostname.
1692 if ( array_key_exists( 'scheme', $parsed_host ) ) {
1693 // If the port isn't in the LDAP URI, use the one in the LDAP port field.
1694 if ( ! array_key_exists( 'port', $parsed_host ) ) {
1695 $parsed_host['port'] = $ldap_port;
1696 }
1697 $ldap_host = Helper::build_url( $parsed_host );
1698 } else {
1699 // Construct the LDAP URI from the provided host and port.
1700 $ldap_host = 'ldap://' . $ldap_host . ':' . $ldap_port;
1701 }
1702
1703 // Create LDAP connection.
1704 $ldap = ldap_connect( $ldap_host );
1705 ldap_set_option( $ldap, LDAP_OPT_PROTOCOL_VERSION, 3 );
1706 ldap_set_option( $ldap, LDAP_OPT_REFERRALS, 0 );
1707
1708 // Fail if we don't have a plausible LDAP URI.
1709 if ( false === $ldap ) {
1710 if ( is_array( $debug ) ) {
1711 /* TRANSLATORS: LDAP Host */
1712 $debug[] = sprintf( __( 'Warning: syntax check failed on host %s in ldap_connect().', 'authorizer' ), $ldap_host );
1713 }
1714 continue;
1715 }
1716
1717 // Attempt to start TLS if that setting is checked and we're not using ldaps protocol.
1718 if ( 1 === intval( $auth_settings['ldap_tls'] ) && false === strpos( $ldap_host, 'ldaps://' ) ) {
1719 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1720 if ( ! @ldap_start_tls( $ldap ) ) {
1721 if ( is_array( $debug ) ) {
1722 /* TRANSLATORS: LDAP Host */
1723 $debug[] = sprintf( __( 'Warning: unable to start TLS on host %s:', 'authorizer' ), $ldap_host );
1724 $debug[] = ldap_error( $ldap );
1725 }
1726 continue;
1727 }
1728 }
1729
1730 // Allow overrides of the LDAP user from filter or constant.
1731 if ( defined( 'AUTHORIZER_LDAP_USER' ) ) {
1732 $auth_settings['ldap_user'] = \AUTHORIZER_LDAP_USER;
1733 }
1734 /**
1735 * Filters the LDAP user used by Authorizer to authenticate.
1736 *
1737 * @since 3.6.2
1738 *
1739 * @param string $ldap_user The stored Oauth2 Client Secret.
1740 */
1741 $auth_settings['ldap_user'] = apply_filters( 'authorizer_ldap_user', $auth_settings['ldap_user'] );
1742
1743 // Allow overrides of the LDAP password from filter or constant.
1744 if ( defined( 'AUTHORIZER_LDAP_PASSWORD' ) ) {
1745 $auth_settings['ldap_password'] = \AUTHORIZER_LDAP_PASSWORD;
1746 }
1747 /**
1748 * Filters the LDAP password used by Authorizer to authenticate.
1749 *
1750 * @since 3.6.2
1751 *
1752 * @param string $ldap_password The stored Oauth2 Client Secret.
1753 */
1754 $auth_settings['ldap_password'] = apply_filters( 'authorizer_ldap_password', $auth_settings['ldap_password'] );
1755
1756 // Set bind credentials; attempt an anonymous bind if not provided.
1757 $bind_rdn = null;
1758 $bind_password = null;
1759 if ( strlen( $auth_settings['ldap_user'] ) > 0 ) {
1760 $bind_rdn = $auth_settings['ldap_user'];
1761 $bind_password = $auth_settings['ldap_password'];
1762
1763 // Decrypt LDAP password if coming from wp_options database (not needed
1764 // if it was provided via constant or filter).
1765 if ( ! defined( 'AUTHORIZER_LDAP_PASSWORD' ) && ! has_filter( 'authorizer_ldap_password' ) ) {
1766 $bind_password = Helper::decrypt( $bind_password );
1767 }
1768
1769 // If the bind user contains the [username] wildcard, replace it with
1770 // the username and password of the user logging in.
1771 if ( false !== strpos( $bind_rdn, '[username]' ) ) {
1772 $bind_rdn = str_replace( '[username]', $username, $bind_rdn );
1773 $bind_password = $password;
1774
1775 if ( is_array( $debug ) ) {
1776 /* TRANSLATORS: LDAP User DN */
1777 $debug[] = sprintf( __( 'Performing bind as user logging in: %s.', 'authorizer' ), $bind_rdn );
1778 }
1779 }
1780 }
1781
1782 // Attempt LDAP bind.
1783 $result = @ldap_bind( $ldap, $bind_rdn, stripslashes( $bind_password ) ); // phpcs:ignore
1784 if ( ! $result ) {
1785 if ( is_array( $debug ) ) {
1786 /* TRANSLATORS: LDAP Host */
1787 $debug[] = sprintf( __( 'Warning: unable to bind on host %1$s using directory user:', 'authorizer' ), $ldap_host );
1788 $debug[] = ldap_error( $ldap );
1789 }
1790
1791 // We failed either an anonymous bind or a bind with a service account,
1792 // so try to bind with the logging in user's credentials before failing.
1793 // Note: multiple search bases can be provided, so iterate through them
1794 // trying to bind as the user logging in.
1795 foreach ( $search_bases as $search_base ) {
1796 $bind_user_dn = $auth_settings['ldap_uid'] . '=' . $username . ',' . $search_base;
1797 $result = @ldap_bind( $ldap, $bind_user_dn, stripslashes( $password ) ); // phpcs:ignore
1798 if ( $result ) {
1799 if ( is_array( $debug ) ) {
1800 /* TRANSLATORS: LDAP User DN */
1801 $debug[] = sprintf( __( 'Successful bind using LDAP user DN %s instead of directory user.', 'authorizer' ), $bind_user_dn );
1802 }
1803
1804 break;
1805 }
1806 }
1807
1808 if ( ! $result ) {
1809 if ( is_array( $debug ) ) {
1810 /* TRANSLATORS: LDAP User */
1811 $debug[] = sprintf( __( 'Failed: password incorrect for LDAP user %s.', 'authorizer' ), $username );
1812 $debug[] = ldap_error( $ldap );
1813 }
1814
1815 // Can't connect to LDAP, so fall back to WordPress authentication.
1816 continue;
1817 }
1818 }
1819
1820 // If we've reached this, we have a valid ldap connection and bind.
1821 $ldap_valid = true;
1822 if ( is_array( $debug ) ) {
1823 /* TRANSLATORS: LDAP Host */
1824 $debug[] = sprintf( __( 'Connected to LDAP host %s.', 'authorizer' ), $ldap_host );
1825 }
1826 break;
1827 }
1828
1829 // Move to next authentication method if we don't have a valid LDAP connection.
1830 if ( ! $ldap_valid ) {
1831 if ( is_array( $debug ) ) {
1832 $debug[] = __( 'Failed: unable to connect to any LDAP host.', 'authorizer' );
1833 }
1834 return null;
1835 }
1836
1837 // Look up the bind DN (and first/last name) of the user trying to
1838 // log in by performing an LDAP search for the login username in
1839 // the field specified in the LDAP settings. This setup is common.
1840 $ldap_attributes_to_retrieve = array( 'dn' );
1841 if ( array_key_exists( 'ldap_attr_first_name', $auth_settings ) && strlen( $auth_settings['ldap_attr_first_name'] ) > 0 ) {
1842 array_push( $ldap_attributes_to_retrieve, $auth_settings['ldap_attr_first_name'] );
1843 }
1844 if ( array_key_exists( 'ldap_attr_last_name', $auth_settings ) && strlen( $auth_settings['ldap_attr_last_name'] ) > 0 ) {
1845 array_push( $ldap_attributes_to_retrieve, $auth_settings['ldap_attr_last_name'] );
1846 }
1847 if ( array_key_exists( 'ldap_attr_email', $auth_settings ) && strlen( $auth_settings['ldap_attr_email'] ) > 0 && substr( $auth_settings['ldap_attr_email'], 0, 1 ) !== '@' ) {
1848 array_push( $ldap_attributes_to_retrieve, Helper::lowercase( $auth_settings['ldap_attr_email'] ) );
1849 }
1850
1851 /**
1852 * Specify additional LDAP user attributes to retrieve during authentication.
1853 * May be used by plugins in `authorizer_user_register`.
1854 *
1855 * @param array $attributes LDAP attributes to retrieve in addition to first name, last name and email.
1856 */
1857 $additional_ldap_attributes_to_retrieve = apply_filters( 'authorizer_additional_ldap_attributes_to_retrieve', array() );
1858 $ldap_attributes_to_retrieve = array_merge( $ldap_attributes_to_retrieve, $additional_ldap_attributes_to_retrieve );
1859
1860 // Create default LDAP search filter. If LDAP email attribute is provided,
1861 // use (|(uid=$username)(mail=$username)) instead (so logins with either a
1862 // username or an email address will work). Otherwise use (uid=$username).
1863 if ( array_key_exists( 'ldap_attr_email', $auth_settings ) && strlen( $auth_settings['ldap_attr_email'] ) > 0 && substr( $auth_settings['ldap_attr_email'], 0, 1 ) !== '@' ) {
1864 $search_filter =
1865 '(|' .
1866 '(' . $auth_settings['ldap_uid'] . '=' . $username . ')' .
1867 '(' . $auth_settings['ldap_attr_email'] . '=' . $username . ')' .
1868 ')';
1869 } else {
1870 $search_filter = '(' . $auth_settings['ldap_uid'] . '=' . $username . ')';
1871 }
1872
1873 // Merge LDAP search filter from plugin settings if it exists.
1874 $ldap_search_filter = trim( $auth_settings['ldap_search_filter'] );
1875 if ( ! empty( $ldap_search_filter ) ) {
1876 $search_filter = '(&' . $search_filter . $ldap_search_filter . ')';
1877 }
1878
1879 /**
1880 * Filter LDAP search filter.
1881 *
1882 * Allows for custom LDAP authentication rules (e.g., restricting login
1883 * access to users in multiple groups, or having certain attributes).
1884 *
1885 * @param string $search_filter The filter to pass to ldap_search().
1886 * @param string $ldap_uid The attribute to compare username against (from Authorizer Settings).
1887 * @param string $username The username attempting to log in.
1888 */
1889 $search_filter = apply_filters( 'authorizer_ldap_search_filter', $search_filter, $auth_settings['ldap_uid'], $username );
1890
1891 if ( is_array( $debug ) ) {
1892 /* TRANSLATORS: LDAP search filter */
1893 $debug[] = sprintf( __( 'Using LDAP search filter: %s', 'authorizer' ), $search_filter );
1894 }
1895
1896 // Multiple search bases can be provided, so iterate through them until a match is found.
1897 foreach ( $search_bases as $search_base ) {
1898 $ldap_search = @ldap_search( // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1899 $ldap,
1900 $search_base,
1901 $search_filter,
1902 $ldap_attributes_to_retrieve
1903 );
1904 $ldap_entries = empty( $ldap_search ) ? array( 'count' => 0 ) : ldap_get_entries( $ldap, $ldap_search );
1905 if ( $ldap_entries['count'] > 0 ) {
1906 if ( is_array( $debug ) ) {
1907 /* TRANSLATORS: 1: LDAP user 2: LDAP search base */
1908 $debug[] = sprintf( __( 'Found user %1$s in search base: %2$s', 'authorizer' ), $username, $search_base );
1909 }
1910 break;
1911 } elseif ( is_array( $debug ) ) {
1912 /* TRANSLATORS: 1: LDAP user 2: LDAP search base */
1913 $debug[] = sprintf( __( 'Failed to find user %1$s in %2$s. Trying next search base.', 'authorizer' ), $username, $search_base );
1914 }
1915 }
1916
1917 // If we didn't find any users in ldap, fall back to WordPress authentication.
1918 if ( $ldap_entries['count'] < 1 ) {
1919 if ( is_array( $debug ) ) {
1920 /* TRANSLATORS: LDAP User */
1921 $debug[] = sprintf( __( 'Failed: no LDAP user %s found.', 'authorizer' ), $username );
1922 }
1923 return null;
1924 }
1925
1926 // Get the bind dn and first/last names; if there are multiple results returned, just get the last one.
1927 for ( $i = 0; $i < $ldap_entries['count']; $i++ ) {
1928 $ldap_user_dn = $ldap_entries[ $i ]['dn'];
1929
1930 // Get user first name and last name.
1931 $ldap_attr_first_name = array_key_exists( 'ldap_attr_first_name', $auth_settings ) ? Helper::lowercase( $auth_settings['ldap_attr_first_name'] ) : '';
1932 if ( strlen( $ldap_attr_first_name ) > 0 && array_key_exists( $ldap_attr_first_name, $ldap_entries[ $i ] ) && $ldap_entries[ $i ][ $ldap_attr_first_name ]['count'] > 0 && strlen( $ldap_entries[ $i ][ $ldap_attr_first_name ][0] ) > 0 ) {
1933 $first_name = $ldap_entries[ $i ][ $ldap_attr_first_name ][0];
1934 }
1935 $ldap_attr_last_name = array_key_exists( 'ldap_attr_last_name', $auth_settings ) ? Helper::lowercase( $auth_settings['ldap_attr_last_name'] ) : '';
1936 if ( strlen( $ldap_attr_last_name ) > 0 && array_key_exists( $ldap_attr_last_name, $ldap_entries[ $i ] ) && $ldap_entries[ $i ][ $ldap_attr_last_name ]['count'] > 0 && strlen( $ldap_entries[ $i ][ $ldap_attr_last_name ][0] ) > 0 ) {
1937 $last_name = $ldap_entries[ $i ][ $ldap_attr_last_name ][0];
1938 }
1939 // Get user email if it is specified in another field.
1940 $ldap_attr_email = array_key_exists( 'ldap_attr_email', $auth_settings ) ? Helper::lowercase( $auth_settings['ldap_attr_email'] ) : '';
1941 if ( strlen( $ldap_attr_email ) > 0 ) {
1942 // If the email attribute starts with an at symbol (@), assume that the
1943 // email domain is manually entered there (instead of a reference to an
1944 // LDAP attribute), and combine that with the username to create the email.
1945 // Otherwise, look up the LDAP attribute for email.
1946 if ( substr( $ldap_attr_email, 0, 1 ) === '@' ) {
1947 $email = Helper::lowercase( $username . $ldap_attr_email );
1948 } elseif ( array_key_exists( $ldap_attr_email, $ldap_entries[ $i ] ) && $ldap_entries[ $i ][ $ldap_attr_email ]['count'] > 0 && strlen( $ldap_entries[ $i ][ $ldap_attr_email ][0] ) > 0 ) {
1949 $email = Helper::lowercase( $ldap_entries[ $i ][ $ldap_attr_email ][0] );
1950 }
1951 }
1952 }
1953
1954 $result = @ldap_bind( $ldap, $ldap_user_dn, stripslashes( $password ) ); // phpcs:ignore
1955 if ( ! $result ) {
1956 if ( is_array( $debug ) ) {
1957 /* TRANSLATORS: LDAP User */
1958 $debug[] = sprintf( __( 'Failed: password incorrect for LDAP user %s.', 'authorizer' ), $username );
1959 }
1960 // We have a real ldap user, but an invalid password. Pass
1961 // through to wp authentication after failing LDAP (since
1962 // this could be a local account that happens to be the
1963 // same name as an LDAP user).
1964 return null;
1965 }
1966
1967 // User successfully authenticated against LDAP, so set the relevant variables.
1968 $externally_authenticated_email = Helper::lowercase( $username . '@' . $domain );
1969
1970 // If an LDAP attribute has been specified as containing the email address, use that instead.
1971 if ( strlen( $email ) > 0 ) {
1972 $externally_authenticated_email = Helper::lowercase( $email );
1973 }
1974
1975 if ( is_array( $debug ) ) {
1976 /* TRANSLATORS: 1: Current time 2: LDAP User 3: LDAP user email */
1977 $debug[] = sprintf( __( '[%1$s] Successfully authenticated user %2$s (%3$s) via LDAP.', 'authorizer' ), wp_date( get_option( 'time_format' ) ), $username, $externally_authenticated_email );
1978 }
1979
1980 return array(
1981 'email' => $externally_authenticated_email,
1982 'username' => $username,
1983 'first_name' => $first_name,
1984 'last_name' => $last_name,
1985 'authenticated_by' => 'ldap',
1986 'ldap_attributes' => $ldap_entries,
1987 );
1988 }
1989
1990
1991 /**
1992 * Fetch the logging out user's external service (so we can log out of it
1993 * below in the wp_logout hook).
1994 *
1995 * Action: clear_auth_cookie
1996 *
1997 * @return void
1998 */
1999 public function pre_logout() {
2000 self::$authenticated_by = get_user_meta( get_current_user_id(), 'authenticated_by', true );
2001
2002 // If we didn't find an authenticated method, check $_REQUEST (if this is a
2003 // pending user facing the "no access" message, their logout link will
2004 // include "external=?" since they don't have a WP_User to attach the
2005 // "authenticated_by" usermeta to).
2006 if ( empty( self::$authenticated_by ) && ! empty( $_REQUEST['external'] ) ) {
2007 self::$authenticated_by = $_REQUEST['external'];
2008 }
2009 }
2010
2011 /**
2012 * Log out of the attached external service.
2013 *
2014 * Action: wp_logout
2015 *
2016 * @param int $user_id ID of the user that was logged out.
2017 *
2018 * @return void
2019 */
2020 public function custom_logout( $user_id ) {
2021 // Grab plugin settings.
2022 $options = Options::get_instance();
2023 $auth_settings = $options->get_all( Helper::SINGLE_CONTEXT, 'allow override' );
2024
2025 // Reset option containing old error messages.
2026 delete_option( 'auth_settings_advanced_login_error' );
2027
2028 // If logged in to CAS, Log out of CAS.
2029 if ( 'cas' === self::$authenticated_by && '1' === $auth_settings['cas'] ) {
2030 if ( ! array_key_exists( 'PHPCAS_CLIENT', $GLOBALS ) || ! array_key_exists( 'phpCAS', $_SESSION ) ) {
2031
2032 /**
2033 * Get the CAS server protocol version (default to SAML 1.1).
2034 *
2035 * @see: https://apereo.github.io/phpCAS/api/group__public.html#gadea9415f40b8d2afc39f140c9be83bbe
2036 */
2037 $cas_version = Options\External\Cas::get_instance()->sanitize_cas_version( $auth_settings['cas_version'] );
2038
2039 /**
2040 * Get valid service URLs for the CAS client to validate against.
2041 *
2042 * @see: https://github.com/apereo/phpCAS/security/advisories/GHSA-8q72-6qq8-xv64
2043 */
2044 $valid_base_urls = Options\External\Cas::get_instance()->get_valid_cas_service_urls();
2045
2046 // Set the CAS client configuration if it hasn't been set already.
2047 if ( 'PROXY' === strtoupper( $auth_settings['cas_method'] ) ) {
2048 \phpCAS::proxy( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
2049 } else {
2050 \phpCAS::client( $cas_version, $auth_settings['cas_host'], intval( $auth_settings['cas_port'] ), $auth_settings['cas_path'], $valid_base_urls );
2051 }
2052 // Allow redirects at the CAS server endpoint (e.g., allow connections
2053 // at an old CAS URL that redirects to a newer CAS URL).
2054 \phpCAS::setExtraCurlOption( CURLOPT_FOLLOWLOCATION, true );
2055 // Restrict logout request origin to the CAS server only (prevent DDOS).
2056 \phpCAS::handleLogoutRequests( true, array( $auth_settings['cas_host'] ) );
2057 }
2058 if ( \phpCAS::isAuthenticated() || \phpCAS::isInitialized() ) {
2059 // Redirect to home page, or specified page if it's been provided.
2060 $redirect_to = site_url( '/' );
2061 if ( ! empty( $_REQUEST['redirect_to'] ) && isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'log-out' ) ) {
2062 $redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
2063 }
2064
2065 \phpCAS::logoutWithRedirectService( $redirect_to );
2066 }
2067 }
2068
2069 // If session token set, log out of Google.
2070 if ( PHP_SESSION_NONE === session_status() ) {
2071 session_start();
2072 }
2073 if ( 'google' === self::$authenticated_by && array_key_exists( 'token', $_SESSION ) ) {
2074 $token = $_SESSION['token'];
2075
2076 // Fetch the Google Client ID (allow overrides from filter or constant).
2077 if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_ID' ) ) {
2078 $auth_settings['google_clientid'] = \AUTHORIZER_GOOGLE_CLIENT_ID;
2079 }
2080 /**
2081 * Filters the Google Client ID used by Authorizer to authenticate.
2082 *
2083 * @since 3.9.0
2084 *
2085 * @param string $google_client_id The stored Google Client ID.
2086 */
2087 $auth_settings['google_clientid'] = apply_filters( 'authorizer_google_client_id', $auth_settings['google_clientid'] );
2088
2089 // Fetch the Google Client Secret (allow overrides from filter or constant).
2090 if ( defined( 'AUTHORIZER_GOOGLE_CLIENT_SECRET' ) ) {
2091 $auth_settings['google_clientsecret'] = \AUTHORIZER_GOOGLE_CLIENT_SECRET;
2092 }
2093 /**
2094 * Filters the Google Client Secret used by Authorizer to authenticate.
2095 *
2096 * @since 3.6.1
2097 *
2098 * @param string $google_client_secret The stored Google Client Secret.
2099 */
2100 $auth_settings['google_clientsecret'] = apply_filters( 'authorizer_google_client_secret', $auth_settings['google_clientsecret'] );
2101
2102 // Build the Google Client.
2103 $client = new \Google_Client();
2104 $client->setApplicationName( 'WordPress' );
2105 $client->setClientId( trim( $auth_settings['google_clientid'] ) );
2106 $client->setClientSecret( trim( $auth_settings['google_clientsecret'] ) );
2107 $client->setRedirectUri( 'postmessage' );
2108
2109 // Revoke the token.
2110 $client->revokeToken( $token );
2111
2112 // Remove the credentials from the user's session.
2113 unset( $_SESSION['token'] );
2114 }
2115
2116 // If logged in via OIDC, perform RP-initiated logout if supported.
2117 if ( 'oidc' === self::$authenticated_by && '1' === $auth_settings['oidc'] ) {
2118 $id_token_hint = get_user_meta( $user_id, 'oidc_id_token', true );
2119 $oidc_server_id = get_user_meta( $user_id, 'oidc_server_id', true );
2120 if ( empty( $oidc_server_id ) ) {
2121 $oidc_server_id = 1;
2122 }
2123
2124 // Get issuer and credentials for the server that was used.
2125 $suffix = $oidc_server_id > 1 ? '_' . $oidc_server_id : '';
2126 $oidc_issuer = $auth_settings[ 'oidc_issuer' . $suffix ] ?? '';
2127 $oidc_client_id = $auth_settings[ 'oidc_client_id' . $suffix ] ?? '';
2128 $oidc_client_secret = $auth_settings[ 'oidc_client_secret' . $suffix ] ?? '';
2129
2130 // Fetch the OIDC Client ID (allow overrides from filter or constant).
2131 // Note: constant/filter overrides are only supported for a single OIDC server.
2132 if ( 1 === $oidc_server_id && defined( 'AUTHORIZER_OIDC_CLIENT_ID' ) ) {
2133 $oidc_client_id = \AUTHORIZER_OIDC_CLIENT_ID;
2134 }
2135 /**
2136 * Filters the OIDC Client ID used by Authorizer to authenticate.
2137 *
2138 * @since 3.11.0
2139 *
2140 * @param string $oidc_client_id The stored OIDC Client ID.
2141 */
2142 if ( 1 === $oidc_server_id ) {
2143 $oidc_client_id = apply_filters( 'authorizer_oidc_client_id', $oidc_client_id );
2144 }
2145
2146 // Fetch the OIDC Client Secret (allow overrides from filter or constant).
2147 // Note: constant/filter overrides are only supported for a single OIDC server.
2148 if ( 1 === $oidc_server_id && defined( 'AUTHORIZER_OIDC_CLIENT_SECRET' ) ) {
2149 $oidc_client_secret = \AUTHORIZER_OIDC_CLIENT_SECRET;
2150 }
2151 /**
2152 * Filters the OIDC Client Secret used by Authorizer to authenticate.
2153 *
2154 * @since 3.11.0
2155 *
2156 * @param string $oidc_client_secret The stored OIDC Client Secret.
2157 */
2158 if ( 1 === $oidc_server_id ) {
2159 $oidc_client_secret = apply_filters( 'authorizer_oidc_client_secret', $oidc_client_secret );
2160 }
2161
2162 if ( ! empty( $oidc_issuer ) && ! empty( $oidc_client_id ) && ! empty( $oidc_client_secret ) ) {
2163 try {
2164 // Initialize OIDC client (library handles discovery automatically).
2165 $oidc = new \Jumbojett\OpenIDConnectClient(
2166 $oidc_issuer,
2167 $oidc_client_id,
2168 $oidc_client_secret
2169 );
2170
2171 // Determine redirect URL.
2172 $redirect_to = site_url( '/' );
2173 if ( ! empty( $_REQUEST['redirect_to'] ) && isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'log-out' ) ) {
2174 $redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
2175 }
2176
2177 // Set session flag to prevent auto-login after logout redirect (only if auto-login is enabled).
2178 // This survives the external redirect through the IDP and works regardless
2179 // of where the IDP redirects back to (wp-login.php, wp-admin, /, etc.).
2180 // Only needed if auto-login is enabled; if disabled, there's no auto-login to prevent.
2181 if ( ! empty( $auth_settings['oidc_auto_login'] ) && in_array( intval( $auth_settings['oidc_auto_login'] ), range( 1, 20 ), true ) ) {
2182 if ( PHP_SESSION_NONE === session_status() ) {
2183 session_start();
2184 }
2185 $_SESSION['oidc_logged_out'] = true;
2186 }
2187
2188 // Clean up user meta before redirect (library's signOut() will exit).
2189 \Authorizer\Options\External\Oidc::get_instance()->delete_oidc_user_meta( $user_id );
2190
2191 // Use library's signOut() method (handles discovery, URL building, and redirect).
2192 // Pass empty string if no ID token (library will still include it in params).
2193 $oidc->signOut( $id_token_hint ?? '', $redirect_to );
2194 // signOut() calls exit, so this line should never be reached.
2195 } catch ( \Jumbojett\OpenIDConnectClientException $e ) {
2196 // Provider doesn't support RP-initiated logout (no end_session_endpoint) or other error.
2197 // Clean up and continue with normal WordPress logout.
2198 \Authorizer\Options\External\Oidc::get_instance()->delete_oidc_user_meta( $user_id );
2199 } catch ( \Exception $e ) {
2200 // Fallback to local logout if RP-initiated logout fails.
2201 // Clean up and continue with normal WordPress logout.
2202 \Authorizer\Options\External\Oidc::get_instance()->delete_oidc_user_meta( $user_id );
2203 }
2204 } else {
2205 // No OIDC issuer/credentials configured - clean up and continue with normal WordPress logout.
2206 \Authorizer\Options\External\Oidc::get_instance()->delete_oidc_user_meta( $user_id );
2207 }
2208 }
2209 }
2210 }
2211