PluginProbe
Authorizer / 3.14.2
Authorizer v3.14.2
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.14.2, at src/authorizer/class-authentication.php

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