Let administrators link Microsoft accounts whose UPN differs from mail
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Privileged accounts are never linked through the settable mail attribute. Two new ways make that workable when UPN and e-mail differ: - "Link Microsoft account" on the profile screen: the signed-in user (nonce, same browser via the state cookie, same user at the callback) signs in with Microsoft once and binds that identity. Existing links can only be removed by an administrator; an object ID bound elsewhere is refused. - "Assigned Microsoft account (UPN)" per user, editable by administrators, used by sign-in and user sync; with an option to remove a link. Sign-in now finds accounts by bound object ID first, then by assigned UPN, then by e-mail, so linked users sign in whatever their addresses. Also: third-audit report (docs/security-audit.md section 7), README section on linking administrator accounts, translations, tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b893e42bc
commit
81b3a74ae5
12 changed files with 1583 additions and 995 deletions
|
|
@ -21,6 +21,8 @@ class M365_Login_Auth {
|
|||
const META_OID = '_m365_login_oid';
|
||||
const META_LAST_LOGIN = '_m365_login_last_login';
|
||||
const META_TID = '_m365_login_tid'; // Tenant the object ID belongs to.
|
||||
const META_UPN = '_m365_login_upn'; // Microsoft account (UPN) assigned by an administrator.
|
||||
const LINK_NONCE = 'm365_login_link';
|
||||
const JWKS_CACHE_TTL = 12 * HOUR_IN_SECONDS;
|
||||
const HTTP_TIMEOUT = 15;
|
||||
|
||||
|
|
@ -316,6 +318,22 @@ class M365_Login_Auth {
|
|||
return $this->authority() . '/discovery/v2.0/keys';
|
||||
}
|
||||
|
||||
/**
|
||||
* URL that links the signed-in user's WordPress account to a Microsoft account.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function link_url() {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'action' => self::ACTION_START,
|
||||
'm365_link' => '1',
|
||||
'_wpnonce' => wp_create_nonce( self::LINK_NONCE ),
|
||||
),
|
||||
wp_login_url()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL that starts the Microsoft login.
|
||||
*
|
||||
|
|
@ -354,6 +372,18 @@ class M365_Login_Auth {
|
|||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- redirect_to is validated with wp_validate_redirect() before use.
|
||||
$redirect_to = isset( $_GET['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ), '' ) : '';
|
||||
|
||||
// "Link my Microsoft account" from the profile: the signed-in user proves ownership of the
|
||||
// WordPress account, the Microsoft sign-in proves ownership of the Microsoft account.
|
||||
$link_user = 0;
|
||||
if ( isset( $_GET['m365_link'] ) ) {
|
||||
$link_nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
|
||||
if ( ! is_user_logged_in() || ! wp_verify_nonce( $link_nonce, self::LINK_NONCE ) ) {
|
||||
$this->fail( 'invalid_state' );
|
||||
}
|
||||
$link_user = get_current_user_id();
|
||||
$redirect_to = admin_url( 'profile.php' );
|
||||
}
|
||||
|
||||
$state = M365_Login_JWT::b64url_encode( random_bytes( 32 ) );
|
||||
$nonce = M365_Login_JWT::b64url_encode( random_bytes( 32 ) );
|
||||
$code_verifier = M365_Login_JWT::b64url_encode( random_bytes( 64 ) );
|
||||
|
|
@ -369,6 +399,7 @@ class M365_Login_Auth {
|
|||
'verifier' => $code_verifier,
|
||||
'cookie' => hash( 'sha256', $cookie_token ),
|
||||
'redirect_to' => $redirect_to,
|
||||
'link_user' => $link_user,
|
||||
'created' => time(),
|
||||
),
|
||||
self::STATE_TTL
|
||||
|
|
@ -496,16 +527,29 @@ class M365_Login_Auth {
|
|||
$this->fail( 'external_identity' );
|
||||
}
|
||||
|
||||
$email = $this->email_from_claims( $claims );
|
||||
if ( '' === $email ) {
|
||||
$this->fail( 'no_email' );
|
||||
$oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : '';
|
||||
|
||||
if ( ! empty( $attempt['link_user'] ) ) {
|
||||
$this->link_account( (int) $attempt['link_user'], $claims, $oid );
|
||||
}
|
||||
|
||||
if ( ! $this->domain_allowed( $email ) ) {
|
||||
$email = $this->email_from_claims( $claims );
|
||||
if ( '' !== $email && ! $this->domain_allowed( $email ) ) {
|
||||
$this->fail( 'domain_not_allowed' );
|
||||
}
|
||||
|
||||
$user = get_user_by( 'email', $email );
|
||||
// 1. An account already bound to this Microsoft identity, 2. an account the administrator
|
||||
// assigned this user principal name to, 3. the e-mail address.
|
||||
$user = $this->find_bound_user( $oid );
|
||||
if ( ! $user ) {
|
||||
$user = $this->find_assigned_user( $claims );
|
||||
}
|
||||
if ( ! $user ) {
|
||||
if ( '' === $email ) {
|
||||
$this->fail( 'no_email' );
|
||||
}
|
||||
$user = get_user_by( 'email', $email );
|
||||
}
|
||||
if ( ! $user instanceof WP_User ) {
|
||||
/** This action is documented in wp-includes/user.php */
|
||||
do_action( 'wp_login_failed', $email, new WP_Error( 'm365_login_no_user', 'No WordPress user with this e-mail address.' ) );
|
||||
|
|
@ -520,8 +564,6 @@ class M365_Login_Auth {
|
|||
$this->fail( 'account_disabled' );
|
||||
}
|
||||
|
||||
$oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : '';
|
||||
|
||||
// Entra group restriction.
|
||||
$group_check = $this->check_groups( $claims, $oid );
|
||||
if ( true !== $group_check ) {
|
||||
|
|
@ -767,11 +809,155 @@ class M365_Login_Auth {
|
|||
* @return bool
|
||||
*/
|
||||
private function may_claim_privileged( $claims, $user ) {
|
||||
$upn = ! empty( $claims['preferred_username'] ) && is_string( $claims['preferred_username'] ) ? strtolower( trim( $claims['preferred_username'] ) ) : '';
|
||||
return '' !== $upn
|
||||
&& ! $this->settings->is_multi_tenant()
|
||||
&& ! $this->is_external_identity( $claims )
|
||||
&& hash_equals( strtolower( $user->user_email ), $upn );
|
||||
$upn = $this->claimed_upn( $claims );
|
||||
if ( '' === $upn || $this->settings->is_multi_tenant() ) {
|
||||
return false;
|
||||
}
|
||||
$assigned = strtolower( (string) get_user_meta( $user->ID, self::META_UPN, true ) );
|
||||
return hash_equals( strtolower( $user->user_email ), $upn ) || ( '' !== $assigned && hash_equals( $assigned, $upn ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* User principal name of a member account from the token ('' for guests/external identities).
|
||||
*
|
||||
* @param array $claims Verified claims.
|
||||
* @return string
|
||||
*/
|
||||
private function claimed_upn( $claims ) {
|
||||
if ( $this->is_external_identity( $claims ) || empty( $claims['preferred_username'] ) || ! is_string( $claims['preferred_username'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return strtolower( trim( $claims['preferred_username'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Account bound to a Microsoft object ID (network-wide), if any.
|
||||
*
|
||||
* @param string $oid Object ID.
|
||||
* @return WP_User|null
|
||||
*/
|
||||
private function find_bound_user( $oid ) {
|
||||
if ( ! $this->settings->get( 'bind_oid' ) || ! M365_Login_Settings::is_guid( $oid ) ) {
|
||||
return null;
|
||||
}
|
||||
$ids = get_users(
|
||||
array(
|
||||
'meta_key' => self::META_OID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $oid, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'fields' => 'ID',
|
||||
'number' => 2,
|
||||
'blog_id' => 0,
|
||||
)
|
||||
);
|
||||
if ( 1 !== count( $ids ) ) {
|
||||
return null; // None, or ambiguous (bound twice by an older version): fall back to the other rules.
|
||||
}
|
||||
$user = get_userdata( (int) $ids[0] );
|
||||
return $user ? $user : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account whose administrator-assigned Microsoft account matches the token's user principal name.
|
||||
*
|
||||
* @param array $claims Verified claims.
|
||||
* @return WP_User|null
|
||||
*/
|
||||
private function find_assigned_user( $claims ) {
|
||||
$upn = $this->claimed_upn( $claims );
|
||||
if ( '' === $upn || $this->settings->is_multi_tenant() ) {
|
||||
return null;
|
||||
}
|
||||
$ids = get_users(
|
||||
array(
|
||||
'meta_key' => self::META_UPN, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $upn, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'fields' => 'ID',
|
||||
'number' => 2,
|
||||
'blog_id' => 0,
|
||||
)
|
||||
);
|
||||
if ( 1 !== count( $ids ) ) {
|
||||
return null;
|
||||
}
|
||||
$user = get_userdata( (int) $ids[0] );
|
||||
return $user ? $user : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the Microsoft identity to the signed-in WordPress user who started "Link my Microsoft account".
|
||||
*
|
||||
* @param int $user_id User who started the link.
|
||||
* @param array $claims Verified claims.
|
||||
* @param string $oid Object ID.
|
||||
*/
|
||||
private function link_account( $user_id, $claims, $oid ) {
|
||||
// The browser must still be signed in as the user who started the link.
|
||||
if ( ! $user_id || get_current_user_id() !== $user_id ) {
|
||||
$this->fail_link( 'link_session' );
|
||||
}
|
||||
if ( ! M365_Login_Settings::is_guid( $oid ) ) {
|
||||
$this->fail_link( 'invalid_token' );
|
||||
}
|
||||
if ( M365_Login_Sync::disabled_info( $user_id ) ) {
|
||||
$this->fail_link( 'account_disabled' );
|
||||
}
|
||||
$stored = strtolower( (string) get_user_meta( $user_id, self::META_OID, true ) );
|
||||
if ( '' !== $stored && ! hash_equals( $stored, $oid ) ) {
|
||||
$this->fail_link( 'link_other' ); // Unlinking is an administrator decision.
|
||||
}
|
||||
$taken = get_users(
|
||||
array(
|
||||
'meta_key' => self::META_OID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $oid, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'exclude' => array( $user_id ),
|
||||
'fields' => 'ID',
|
||||
'number' => 1,
|
||||
'blog_id' => 0,
|
||||
)
|
||||
);
|
||||
if ( ! empty( $taken ) ) {
|
||||
$this->fail_link( 'oid_mismatch' );
|
||||
}
|
||||
|
||||
update_user_meta( $user_id, self::META_OID, $oid );
|
||||
if ( isset( $claims['tid'] ) && M365_Login_Settings::is_guid( (string) $claims['tid'] ) ) {
|
||||
update_user_meta( $user_id, self::META_TID, strtolower( (string) $claims['tid'] ) );
|
||||
}
|
||||
$this->log( sprintf( 'User #%d linked a Microsoft account from the profile.', $user_id ) );
|
||||
|
||||
/**
|
||||
* Fires after a user linked a Microsoft account from the profile screen.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param array $claims Verified claims.
|
||||
*/
|
||||
do_action( 'm365_login_account_linked', $user_id, $claims );
|
||||
|
||||
wp_safe_redirect( add_query_arg( 'm365_linked', '1', admin_url( 'profile.php' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a failed profile link with a message on the profile screen.
|
||||
*
|
||||
* @param string $code Error code.
|
||||
*/
|
||||
private function fail_link( $code ) {
|
||||
$this->clear_state_cookie();
|
||||
nocache_headers();
|
||||
wp_safe_redirect( add_query_arg( 'm365_link_error', rawurlencode( $code ), admin_url( 'profile.php' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated message for a login/link error code.
|
||||
*
|
||||
* @param string $code Code.
|
||||
* @return string
|
||||
*/
|
||||
public function error_message( $code ) {
|
||||
$messages = $this->error_messages();
|
||||
return isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1081,7 +1267,9 @@ class M365_Login_Auth {
|
|||
'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
|
||||
'too_many_attempts' => __( 'Too many sign-in attempts from your connection. Please wait a few minutes and try again.', 'm365-login' ),
|
||||
'account_disabled' => __( 'This account has been deactivated.', 'm365-login' ),
|
||||
'privileged_unlinked' => __( 'For security reasons this administrator account can only be linked to a Microsoft account whose user principal name equals the WordPress e-mail address. Please contact an administrator.', 'm365-login' ),
|
||||
'privileged_unlinked' => __( 'For security reasons this administrator account is not linked automatically. Sign in once with your password and click "Link Microsoft account" on your profile page – or ask an administrator to enter your Microsoft account (user principal name) in your WordPress profile.', 'm365-login' ),
|
||||
'link_session' => __( 'The link could not be completed because you are no longer signed in to WordPress. Please sign in and try again.', 'm365-login' ),
|
||||
'link_other' => __( 'Your WordPress account is already linked to a different Microsoft account. An administrator can remove the link in your profile.', 'm365-login' ),
|
||||
'external_identity' => __( 'Guest and external accounts cannot sign in here.', 'm365-login' ),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ class M365_Login_Sync {
|
|||
add_action( 'admin_post_' . self::POST_STATE, array( $this, 'handle_user_state' ) );
|
||||
add_action( 'show_user_profile', array( $this, 'profile_section' ) );
|
||||
add_action( 'edit_user_profile', array( $this, 'profile_section' ) );
|
||||
add_action( 'personal_options_update', array( $this, 'save_profile' ) );
|
||||
add_action( 'edit_user_profile_update', array( $this, 'save_profile' ) );
|
||||
add_action( 'admin_notices', array( $this, 'user_state_notice' ) );
|
||||
}
|
||||
|
||||
|
|
@ -529,7 +531,8 @@ class M365_Login_Sync {
|
|||
}
|
||||
|
||||
// Large directories: keep memory flat and the run lock fresh.
|
||||
if ( 0 === ++$done % 250 ) {
|
||||
++$done;
|
||||
if ( 0 === $done % 250 ) {
|
||||
if ( function_exists( 'wp_cache_flush_runtime' ) ) {
|
||||
wp_cache_flush_runtime();
|
||||
}
|
||||
|
|
@ -739,9 +742,13 @@ class M365_Login_Sync {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Not linked yet: match an existing account by e-mail address.
|
||||
// Not linked yet: the account an administrator assigned this user principal name to,
|
||||
// otherwise an existing account with the same e-mail address.
|
||||
if ( ! $user ) {
|
||||
$by_mail = get_user_by( 'email', $email );
|
||||
$by_mail = $this->assigned_user( $person );
|
||||
if ( ! $by_mail ) {
|
||||
$by_mail = get_user_by( 'email', $email );
|
||||
}
|
||||
if ( $by_mail instanceof WP_User ) {
|
||||
$stored = strtolower( (string) get_user_meta( $by_mail->ID, M365_Login_Auth::META_OID, true ) );
|
||||
if ( '' !== $stored && $stored !== $oid ) {
|
||||
|
|
@ -751,7 +758,7 @@ class M365_Login_Sync {
|
|||
}
|
||||
if ( ! $this->may_link( $by_mail, $person, $email ) ) {
|
||||
/* translators: %s: e-mail address */
|
||||
$this->skip( sprintf( __( '%s: privileged WordPress account – it is only linked when the Microsoft user principal name equals its e-mail address (member account, no guest). Skipped.', 'm365-login' ), $email ) );
|
||||
$this->skip( sprintf( __( '%s: privileged WordPress account – linked only when the Microsoft user principal name equals its e-mail address or the Microsoft account assigned in its profile, or when the person links it from the profile. Skipped.', 'm365-login' ), $email ) );
|
||||
return null;
|
||||
}
|
||||
$user = $by_mail;
|
||||
|
|
@ -1282,9 +1289,51 @@ class M365_Login_Sync {
|
|||
if ( ! self::is_privileged( $user ) ) {
|
||||
return true;
|
||||
}
|
||||
$upn = isset( $person['userPrincipalName'] ) ? strtolower( (string) $person['userPrincipalName'] ) : '';
|
||||
$upn = self::member_upn( $person );
|
||||
if ( '' === $upn ) {
|
||||
return false;
|
||||
}
|
||||
$assigned = strtolower( (string) get_user_meta( $user->ID, M365_Login_Auth::META_UPN, true ) );
|
||||
return ( '' !== $assigned && $assigned === $upn ) || ( strtolower( $user->user_email ) === $upn && $upn === $email );
|
||||
}
|
||||
|
||||
/**
|
||||
* User principal name of a member (not a guest), lowercase, or ''.
|
||||
*
|
||||
* @param array $person Graph user.
|
||||
* @return string
|
||||
*/
|
||||
private static function member_upn( $person ) {
|
||||
$upn = isset( $person['userPrincipalName'] ) ? strtolower( trim( (string) $person['userPrincipalName'] ) ) : '';
|
||||
$guest = isset( $person['userType'] ) && 'Guest' === $person['userType'];
|
||||
return ! $guest && '' !== $upn && false === strpos( $upn, '#ext#' ) && strtolower( $user->user_email ) === $upn && $upn === $email;
|
||||
return $guest || false !== strpos( $upn, '#ext#' ) ? '' : $upn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account an administrator assigned this person's user principal name to.
|
||||
*
|
||||
* @param array $person Graph user.
|
||||
* @return WP_User|null
|
||||
*/
|
||||
private function assigned_user( $person ) {
|
||||
$upn = self::member_upn( $person );
|
||||
if ( '' === $upn ) {
|
||||
return null;
|
||||
}
|
||||
$ids = get_users(
|
||||
array(
|
||||
'meta_key' => M365_Login_Auth::META_UPN, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $upn, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'fields' => 'ID',
|
||||
'number' => 2,
|
||||
'blog_id' => 0,
|
||||
)
|
||||
);
|
||||
if ( 1 !== count( $ids ) ) {
|
||||
return null;
|
||||
}
|
||||
$user = get_userdata( (int) $ids[0] );
|
||||
return $user ? $user : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1887,7 +1936,7 @@ class M365_Login_Sync {
|
|||
}
|
||||
$stored = get_user_meta( $user->ID, self::META_PHOTO, true );
|
||||
if ( is_array( $stored ) && ! empty( $stored['file'] ) && self::is_photo_file( $stored['file'] ) ) {
|
||||
$uploads = wp_get_upload_dir();
|
||||
$uploads = wp_get_upload_dir();
|
||||
$fields[ __( 'Profile photo', 'm365-login' ) ] = trailingslashit( $uploads['baseurl'] ) . $stored['file'];
|
||||
}
|
||||
$last = (int) get_user_meta( $user->ID, self::META_LAST_SYNC, true );
|
||||
|
|
@ -2188,6 +2237,16 @@ class M365_Login_Sync {
|
|||
* Confirmation after a row action.
|
||||
*/
|
||||
public function user_state_notice() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- display only.
|
||||
if ( isset( $_GET['m365_linked'] ) ) {
|
||||
printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html__( 'Your Microsoft account is now linked. From now on you can sign in with the Microsoft button.', 'm365-login' ) );
|
||||
}
|
||||
$link_error = isset( $_GET['m365_link_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_link_error'] ) ) : '';
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
if ( '' !== $link_error ) {
|
||||
printf( '<div class="notice notice-error is-dismissible"><p>%s</p></div>', esc_html( M365_Login::instance()->auth->error_message( $link_error ) ) );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
|
||||
$state = isset( $_GET['m365_user_state'] ) ? sanitize_key( wp_unslash( $_GET['m365_user_state'] ) ) : '';
|
||||
if ( '' === $state ) {
|
||||
|
|
@ -2205,8 +2264,11 @@ class M365_Login_Sync {
|
|||
* @param WP_User $user User being edited.
|
||||
*/
|
||||
public function profile_section( $user ) {
|
||||
$oid = (string) get_user_meta( $user->ID, M365_Login_Auth::META_OID, true );
|
||||
if ( '' === $oid && ! self::disabled_info( $user->ID ) ) {
|
||||
$oid = (string) get_user_meta( $user->ID, M365_Login_Auth::META_OID, true );
|
||||
$own = get_current_user_id() === $user->ID;
|
||||
$is_admin = current_user_can( M365_Login_Admin::capability() ) && current_user_can( 'edit_user', $user->ID );
|
||||
$can_link = $own && '' === $oid && $this->settings->is_configured() && ! self::disabled_info( $user->ID );
|
||||
if ( '' === $oid && ! self::disabled_info( $user->ID ) && ! $can_link && ! $is_admin ) {
|
||||
return;
|
||||
}
|
||||
$rows = array();
|
||||
|
|
@ -2242,9 +2304,36 @@ class M365_Login_Sync {
|
|||
$rows[ (string) $attribute['label'] ] = $value;
|
||||
}
|
||||
}
|
||||
$assigned = (string) get_user_meta( $user->ID, M365_Login_Auth::META_UPN, true );
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Microsoft 365', 'm365-login' ); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Microsoft account', 'm365-login' ); ?></th>
|
||||
<td>
|
||||
<?php if ( '' !== $oid ) : ?>
|
||||
<?php esc_html_e( 'Linked', 'm365-login' ); ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( 'Not linked', 'm365-login' ); ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( $can_link ) : ?>
|
||||
<p><a class="button" href="<?php echo esc_url( M365_Login::instance()->auth->link_url() ); ?>"><?php esc_html_e( 'Link Microsoft account', 'm365-login' ); ?></a></p>
|
||||
<p class="description"><?php esc_html_e( 'You sign in with Microsoft once; afterwards this WordPress account is bound to that Microsoft account, even if its user principal name differs from your e-mail address.', 'm365-login' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ( $is_admin ) : ?>
|
||||
<tr>
|
||||
<th scope="row"><label for="m365-login-upn"><?php esc_html_e( 'Assigned Microsoft account (UPN)', 'm365-login' ); ?></label></th>
|
||||
<td>
|
||||
<input type="text" class="regular-text code" id="m365-login-upn" name="m365_login_upn" value="<?php echo esc_attr( $assigned ); ?>" placeholder="name@contoso.onmicrosoft.com" autocomplete="off" spellcheck="false" />
|
||||
<p class="description"><?php esc_html_e( 'Optional. The user principal name of the Microsoft account that belongs to this user. Sign-in and user sync link exactly this account – needed for administrators whose user principal name differs from their WordPress e-mail address.', 'm365-login' ); ?></p>
|
||||
<?php if ( '' !== $oid ) : ?>
|
||||
<label><input type="checkbox" name="m365_login_unlink" value="1" /> <?php esc_html_e( 'Remove the link to the Microsoft account', 'm365-login' ); ?></label>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ( $rows as $label => $value ) : ?>
|
||||
<tr>
|
||||
<th scope="row"><?php echo esc_html( $label ); ?></th>
|
||||
|
|
@ -2252,7 +2341,36 @@ class M365_Login_Sync {
|
|||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<p class="description"><?php esc_html_e( 'These values are managed by the Microsoft 365 user sync and overwritten on the next run.', 'm365-login' ); ?></p>
|
||||
<?php if ( $rows ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'These values are managed by the Microsoft 365 user sync and overwritten on the next run.', 'm365-login' ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the administrator fields of the Microsoft 365 profile section.
|
||||
*
|
||||
* Core has verified the "update-user_{$user_id}" nonce before these hooks run.
|
||||
*
|
||||
* @param int $user_id User being saved.
|
||||
*/
|
||||
public function save_profile( $user_id ) {
|
||||
if ( ! current_user_can( M365_Login_Admin::capability() ) || ! current_user_can( 'edit_user', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- verified by core (update-user_{$user_id}).
|
||||
if ( isset( $_POST['m365_login_upn'] ) && is_string( $_POST['m365_login_upn'] ) ) {
|
||||
$upn = strtolower( trim( sanitize_text_field( wp_unslash( $_POST['m365_login_upn'] ) ) ) );
|
||||
if ( '' === $upn ) {
|
||||
delete_user_meta( $user_id, M365_Login_Auth::META_UPN );
|
||||
} elseif ( is_email( $upn ) ) {
|
||||
update_user_meta( $user_id, M365_Login_Auth::META_UPN, $upn );
|
||||
}
|
||||
}
|
||||
if ( ! empty( $_POST['m365_login_unlink'] ) ) {
|
||||
delete_user_meta( $user_id, M365_Login_Auth::META_OID );
|
||||
delete_user_meta( $user_id, M365_Login_Auth::META_TID );
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue