Add Entra group restriction, button-only mode and detailed README
Groups: a Graph-backed picker on the Security tab (search by name or paste object IDs) stores allowed group IDs. During sign-in membership is read from the ID token's groups claim when present, otherwise verified through Microsoft Graph checkMemberGroups (transitive). Verification failures refuse the sign-in. Button-only mode: hides the password form and the lost-password link and rejects password sign-ins on wp-login.php via the authenticate filter. A generated, rate-limited fallback key re-enables the form for 30 minutes per browser; M365_LOGIN_DISABLE_BUTTON_ONLY switches the mode off from wp-config.php. Also: new German-language README with sequence diagram, settings reference, troubleshooting and hook examples; readme.txt external services section now covers Microsoft Graph; translations updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
This commit is contained in:
parent
1517e7e3bc
commit
1202283eda
20 changed files with 2241 additions and 517 deletions
|
|
@ -14,8 +14,9 @@ class M365_Login_Admin {
|
|||
|
||||
const PAGE = 'm365-login';
|
||||
const GROUP = 'm365_login';
|
||||
const AJAX_TEST = 'm365_login_test_connection';
|
||||
const NONCE_TEST = 'm365_login_test';
|
||||
const AJAX_TEST = 'm365_login_test_connection';
|
||||
const AJAX_GROUPS = 'm365_login_search_groups';
|
||||
const NONCE_TEST = 'm365_login_test';
|
||||
|
||||
/**
|
||||
* Settings.
|
||||
|
|
@ -31,6 +32,13 @@ class M365_Login_Admin {
|
|||
*/
|
||||
private $auth;
|
||||
|
||||
/**
|
||||
* Graph client.
|
||||
*
|
||||
* @var M365_Login_Graph
|
||||
*/
|
||||
private $graph;
|
||||
|
||||
/**
|
||||
* Screen hook suffix.
|
||||
*
|
||||
|
|
@ -43,15 +51,19 @@ class M365_Login_Admin {
|
|||
*
|
||||
* @param M365_Login_Settings $settings Settings.
|
||||
* @param M365_Login_Auth $auth Auth.
|
||||
* @param M365_Login_Graph $graph Graph client.
|
||||
*/
|
||||
public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth ) {
|
||||
public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth, M365_Login_Graph $graph ) {
|
||||
$this->settings = $settings;
|
||||
$this->auth = $auth;
|
||||
$this->graph = $graph;
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'menu' ) );
|
||||
add_action( 'admin_init', array( $this, 'register' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
|
||||
add_action( 'wp_ajax_' . self::AJAX_TEST, array( $this, 'ajax_test_connection' ) );
|
||||
add_action( 'wp_ajax_' . self::AJAX_GROUPS, array( $this, 'ajax_search_groups' ) );
|
||||
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
|
||||
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +139,7 @@ class M365_Login_Admin {
|
|||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( self::NONCE_TEST ),
|
||||
'action' => self::AJAX_TEST,
|
||||
'groupAction' => self::AJAX_GROUPS,
|
||||
'defaultLogo' => M365_Login_Button::microsoft_logo(),
|
||||
'i18n' => array(
|
||||
'chooseIcon' => __( 'Choose button icon', 'm365-login' ),
|
||||
|
|
@ -135,6 +148,12 @@ class M365_Login_Admin {
|
|||
'copy' => __( 'Copy', 'm365-login' ),
|
||||
'testing' => __( 'Testing…', 'm365-login' ),
|
||||
'testFailed' => __( 'The tenant could not be reached. Check the tenant ID and the server’s outgoing connections.', 'm365-login' ),
|
||||
'noGroups' => __( 'No groups found.', 'm365-login' ),
|
||||
'searching' => __( 'Searching…', 'm365-login' ),
|
||||
'add' => __( 'Add', 'm365-login' ),
|
||||
'remove' => __( 'Remove', 'm365-login' ),
|
||||
'saveFirst' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ),
|
||||
'confirmKey' => __( 'Generate a new fallback key on save? The old link stops working.', 'm365-login' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
|
@ -179,6 +198,32 @@ class M365_Login_Admin {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: search Entra groups through Microsoft Graph.
|
||||
*/
|
||||
public function ajax_search_groups() {
|
||||
check_ajax_referer( self::NONCE_TEST, 'nonce' );
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
|
||||
}
|
||||
if ( ! $this->settings->is_configured() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ) ) );
|
||||
}
|
||||
|
||||
$query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
|
||||
$groups = $this->graph->search_groups( mb_substr( $query, 0, 100 ) );
|
||||
|
||||
if ( is_wp_error( $groups ) ) {
|
||||
$message = $groups->get_error_message();
|
||||
if ( false !== stripos( $message, 'Authorization_RequestDenied' ) || false !== stripos( $message, 'Insufficient privileges' ) ) {
|
||||
$message = __( 'Microsoft Graph refused the request. Grant the application permission "GroupMember.Read.All" (or "Directory.Read.All") with admin consent in Entra ID.', 'm365-login' );
|
||||
}
|
||||
wp_send_json_error( array( 'message' => $message ) );
|
||||
}
|
||||
|
||||
wp_send_json_success( array( 'groups' => $groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings screen.
|
||||
*/
|
||||
|
|
@ -402,6 +447,79 @@ class M365_Login_Admin {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m365-card">
|
||||
<h2 class="m365-card__title"><?php esc_html_e( 'Allowed Entra groups (optional)', 'm365-login' ); ?></h2>
|
||||
<p class="m365-card__intro"><?php esc_html_e( 'Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count.', 'm365-login' ); ?></p>
|
||||
|
||||
<div class="m365-field">
|
||||
<label for="m365-group-search"><?php esc_html_e( 'Search groups', 'm365-login' ); ?></label>
|
||||
<div class="m365-field__row">
|
||||
<input type="search" id="m365-group-search" class="regular-text" placeholder="<?php esc_attr_e( 'Type a group name or paste an object ID…', 'm365-login' ); ?>" autocomplete="off" <?php disabled( ! $configured ); ?> />
|
||||
<button type="button" class="button" id="m365-group-search-btn" <?php disabled( ! $configured ); ?>><?php esc_html_e( 'Search', 'm365-login' ); ?></button>
|
||||
</div>
|
||||
<?php if ( ! $configured ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'Save the connection settings first, then search for groups.', 'm365-login' ); ?></p>
|
||||
<?php else : ?>
|
||||
<p class="description"><?php esc_html_e( 'Needs the application permission "GroupMember.Read.All" with admin consent. Without it you can still paste group object IDs.', 'm365-login' ); ?></p>
|
||||
<?php endif; ?>
|
||||
<div id="m365-group-results" class="m365-group-results" hidden></div>
|
||||
</div>
|
||||
|
||||
<div class="m365-field">
|
||||
<span class="m365-field__label"><?php esc_html_e( 'Selected groups', 'm365-login' ); ?></span>
|
||||
<ul id="m365-group-list" class="m365-group-list" data-empty="<?php esc_attr_e( 'No groups selected – every matched user may sign in.', 'm365-login' ); ?>">
|
||||
<?php foreach ( $this->settings->allowed_groups() as $gid => $gname ) : ?>
|
||||
<li class="m365-group-chip" data-id="<?php echo esc_attr( $gid ); ?>">
|
||||
<span class="m365-group-chip__name"><?php echo esc_html( $gname ); ?></span>
|
||||
<code class="m365-group-chip__id"><?php echo esc_html( $gid ); ?></code>
|
||||
<input type="hidden" name="<?php echo esc_attr( $option . '[allowed_groups][' . $gid . ']' ); ?>" value="<?php echo esc_attr( $gname ); ?>" />
|
||||
<button type="button" class="m365-group-chip__remove" aria-label="<?php esc_attr_e( 'Remove', 'm365-login' ); ?>">×</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="description"><?php esc_html_e( 'Membership is read from the "groups" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission "User.Read.All" or "Directory.Read.All"). If neither works, the sign-in is refused.', 'm365-login' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m365-card">
|
||||
<h2 class="m365-card__title"><?php esc_html_e( 'Button-only mode', 'm365-login' ); ?></h2>
|
||||
<p class="m365-card__intro"><?php esc_html_e( 'Hide the username/password form and the "Lost your password?" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected.', 'm365-login' ); ?></p>
|
||||
|
||||
<label class="m365-check m365-check--block">
|
||||
<input type="checkbox" name="<?php echo $field( 'button_only' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['button_only'] ); ?> id="m365-button-only" />
|
||||
<span>
|
||||
<strong><?php esc_html_e( 'Show only the Microsoft button on the login page', 'm365-login' ); ?></strong>
|
||||
<em><?php esc_html_e( 'Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this.', 'm365-login' ); ?></em>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="m365-fallback">
|
||||
<span class="m365-field__label"><?php esc_html_e( 'Fallback link (keep it secret)', 'm365-login' ); ?></span>
|
||||
<p class="description"><?php esc_html_e( 'Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks.', 'm365-login' ); ?></p>
|
||||
<?php if ( '' !== $this->settings->fallback_url() ) : ?>
|
||||
<div class="m365-copy">
|
||||
<code id="m365-fallback-url"><?php echo esc_html( $this->settings->fallback_url() ); ?></code>
|
||||
<button type="button" class="button button-small m365-copy__button" data-copy="m365-fallback-url"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button>
|
||||
</div>
|
||||
<label class="m365-check m365-check--inline">
|
||||
<input type="checkbox" name="<?php echo $field( 'fallback_regenerate' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" id="m365-fallback-regenerate" />
|
||||
<?php esc_html_e( 'Generate a new key when saving', 'm365-login' ); ?>
|
||||
</label>
|
||||
<?php else : ?>
|
||||
<p class="m365-inline-result"><?php esc_html_e( 'A key is generated automatically the first time you save these settings.', 'm365-login' ); ?></p>
|
||||
<?php endif; ?>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: PHP constant */
|
||||
esc_html__( 'Emergency switch: add %s to wp-config.php to disable button-only mode entirely.', 'm365-login' ),
|
||||
'<code>define( \'M365_LOGIN_DISABLE_BUTTON_ONLY\', true );</code>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m365-card m365-card--muted">
|
||||
<h2 class="m365-card__title"><?php esc_html_e( 'What the plugin does to keep sign-ins safe', 'm365-login' ); ?></h2>
|
||||
<ul class="m365-list">
|
||||
|
|
@ -444,6 +562,7 @@ class M365_Login_Admin {
|
|||
<li><?php esc_html_e( 'Under Token configuration add the optional claim "email" for ID tokens (recommended), then save this page.', 'm365-login' ); ?></li>
|
||||
</ol>
|
||||
<p class="description"><?php esc_html_e( 'Required API permission: openid, profile, email (delegated) – granted by default.', 'm365-login' ); ?></p>
|
||||
<p class="description"><?php esc_html_e( 'Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent.', 'm365-login' ); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="m365-card m365-card--muted">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue