Certificate (RFC 7523 client assertion) as an alternative to the client secret: one-click generation of a 3072-bit RSA key pair with a self-signed certificate, .cer download (public part only), own PEM upload with validation, expiry display, encrypted key storage. Both the authorization code exchange and the Graph client-credentials request use the selected method. Step-by-step guides for secret, certificate and the app registration are shown in the settings. Security audit (docs/security-audit.md) and fixes: - Multi-tenant mode ignored the unverified email claim: matching now uses the UPN only, or the email claim when xms_edov is true. - Login starts are rate limited per client (30 per 10 minutes). - Optional trusted proxy header for client IPs (M365_LOGIN_CLIENT_IP_HEADER / filter). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
794 lines
49 KiB
PHP
794 lines
49 KiB
PHP
<?php
|
||
/**
|
||
* Admin settings screen.
|
||
*
|
||
* @package M365_Login
|
||
*/
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
/**
|
||
* Registers and renders the settings page.
|
||
*/
|
||
class M365_Login_Admin {
|
||
|
||
const PAGE = 'm365-login';
|
||
const GROUP = 'm365_login';
|
||
const AJAX_TEST = 'm365_login_test_connection';
|
||
const AJAX_GROUPS = 'm365_login_search_groups';
|
||
const AJAX_CERT = 'm365_login_certificate';
|
||
const POST_CERT = 'm365_login_download_cert';
|
||
const NONCE_TEST = 'm365_login_test';
|
||
|
||
/**
|
||
* Settings.
|
||
*
|
||
* @var M365_Login_Settings
|
||
*/
|
||
private $settings;
|
||
|
||
/**
|
||
* Auth component (for endpoint URLs and discovery).
|
||
*
|
||
* @var M365_Login_Auth
|
||
*/
|
||
private $auth;
|
||
|
||
/**
|
||
* Graph client.
|
||
*
|
||
* @var M365_Login_Graph
|
||
*/
|
||
private $graph;
|
||
|
||
/**
|
||
* Screen hook suffix.
|
||
*
|
||
* @var string
|
||
*/
|
||
private $hook = '';
|
||
|
||
/**
|
||
* Constructor.
|
||
*
|
||
* @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, 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( 'wp_ajax_' . self::AJAX_CERT, array( $this, 'ajax_certificate' ) );
|
||
add_action( 'admin_post_' . self::POST_CERT, array( $this, 'download_certificate' ) );
|
||
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
|
||
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
|
||
}
|
||
|
||
/**
|
||
* Adds the menu entry under Settings.
|
||
*/
|
||
public function menu() {
|
||
$this->hook = add_options_page(
|
||
__( 'M365 Login', 'm365-login' ),
|
||
__( 'M365 Login', 'm365-login' ),
|
||
'manage_options',
|
||
self::PAGE,
|
||
array( $this, 'render' )
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Registers the option with the Settings API.
|
||
*/
|
||
public function register() {
|
||
register_setting(
|
||
self::GROUP,
|
||
M365_LOGIN_OPTION,
|
||
array(
|
||
'type' => 'array',
|
||
'sanitize_callback' => array( $this->settings, 'sanitize' ),
|
||
'default' => $this->settings->defaults(),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Nudges administrators to finish the setup.
|
||
*/
|
||
public function setup_notice() {
|
||
if ( $this->settings->is_configured() || ! current_user_can( 'manage_options' ) ) {
|
||
return;
|
||
}
|
||
$screen = get_current_screen();
|
||
if ( $screen && $this->hook === $screen->id ) {
|
||
return;
|
||
}
|
||
if ( ! $screen || ! in_array( $screen->id, array( 'plugins', 'dashboard' ), true ) ) {
|
||
return;
|
||
}
|
||
printf(
|
||
'<div class="notice notice-info is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
|
||
esc_html__( 'M365 Login is active but not connected to Microsoft Entra ID yet.', 'm365-login' ),
|
||
esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ),
|
||
esc_html__( 'Open the settings', 'm365-login' )
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Loads assets on our screen only.
|
||
*
|
||
* @param string $hook Current screen hook.
|
||
*/
|
||
public function enqueue( $hook ) {
|
||
if ( $hook !== $this->hook ) {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_media();
|
||
wp_enqueue_style( 'wp-color-picker' );
|
||
wp_enqueue_style( 'm365-login-admin', M365_LOGIN_URL . 'assets/css/admin.css', array( 'wp-color-picker' ), M365_LOGIN_VERSION );
|
||
wp_enqueue_script( 'm365-login-admin', M365_LOGIN_URL . 'assets/js/admin.js', array( 'jquery', 'wp-color-picker' ), M365_LOGIN_VERSION, true );
|
||
|
||
wp_localize_script(
|
||
'm365-login-admin',
|
||
'm365LoginAdmin',
|
||
array(
|
||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||
'nonce' => wp_create_nonce( self::NONCE_TEST ),
|
||
'action' => self::AJAX_TEST,
|
||
'groupAction' => self::AJAX_GROUPS,
|
||
'certAction' => self::AJAX_CERT,
|
||
'defaultLogo' => M365_Login_Button::microsoft_logo(),
|
||
'i18n' => array(
|
||
'chooseIcon' => __( 'Choose button icon', 'm365-login' ),
|
||
'useIcon' => __( 'Use this icon', 'm365-login' ),
|
||
'copied' => __( 'Copied!', 'm365-login' ),
|
||
'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' ),
|
||
'generating' => __( 'Generating a 3072-bit key pair, this takes a moment…', 'm365-login' ),
|
||
'confirmCert' => __( 'Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID.', 'm365-login' ),
|
||
'confirmCertRemove' => __( 'Remove the stored certificate when saving? Sign-in with the certificate method stops working.', 'm365-login' ),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* AJAX: fetch the OpenID configuration for the tenant typed into the form.
|
||
*/
|
||
public function ajax_test_connection() {
|
||
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 );
|
||
}
|
||
|
||
$tenant = isset( $_POST['tenant'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['tenant'] ) ) ) : '';
|
||
if ( '' === $tenant || ! M365_Login_Settings::is_valid_tenant( $tenant ) ) {
|
||
wp_send_json_error( array( 'message' => __( 'Please enter a valid tenant ID first.', 'm365-login' ) ) );
|
||
}
|
||
|
||
$url = 'https://login.microsoftonline.com/' . rawurlencode( $tenant ) . '/v2.0/.well-known/openid-configuration';
|
||
$response = wp_remote_get( $url, array( 'timeout' => M365_Login_Auth::HTTP_TIMEOUT ) );
|
||
if ( is_wp_error( $response ) ) {
|
||
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
|
||
}
|
||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||
if ( 200 !== $code || ! is_array( $body ) || empty( $body['issuer'] ) ) {
|
||
wp_send_json_error(
|
||
array(
|
||
/* translators: %d: HTTP status code */
|
||
'message' => sprintf( __( 'Microsoft answered with HTTP %d. Is the tenant ID correct?', 'm365-login' ), $code ),
|
||
)
|
||
);
|
||
}
|
||
|
||
wp_send_json_success(
|
||
array(
|
||
'issuer' => esc_url_raw( $body['issuer'] ),
|
||
'endpoint' => isset( $body['authorization_endpoint'] ) ? esc_url_raw( $body['authorization_endpoint'] ) : '',
|
||
'message' => __( 'Tenant reachable. The OpenID configuration was loaded successfully.', 'm365-login' ),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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 ) );
|
||
}
|
||
|
||
/**
|
||
* AJAX: generate a new self-signed certificate and store it (key encrypted).
|
||
*/
|
||
public function ajax_certificate() {
|
||
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 );
|
||
}
|
||
$op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : '';
|
||
if ( 'generate' !== $op ) {
|
||
wp_send_json_error( array( 'message' => __( 'Unknown operation.', 'm365-login' ) ) );
|
||
}
|
||
|
||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||
$pair = M365_Login_Certificate::generate( is_string( $host ) ? $host : 'wordpress' );
|
||
if ( is_wp_error( $pair ) ) {
|
||
wp_send_json_error( array( 'message' => $pair->get_error_message() ) );
|
||
}
|
||
$stored = $this->settings->store_certificate( $pair );
|
||
if ( is_wp_error( $stored ) ) {
|
||
wp_send_json_error( array( 'message' => $stored->get_error_message() ) );
|
||
}
|
||
$this->graph->flush_token();
|
||
|
||
$info = M365_Login_Certificate::info( $pair['certificate'] );
|
||
wp_send_json_success(
|
||
array(
|
||
'message' => __( 'Certificate generated and stored. Download the .cer file and upload it in Entra ID.', 'm365-login' ),
|
||
'thumbprint' => $info ? $info['thumbprint'] : '',
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Sends the public certificate as a .cer download (never the private key).
|
||
*/
|
||
public function download_certificate() {
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_die( esc_html__( 'You are not allowed to do this.', 'm365-login' ), 403 );
|
||
}
|
||
check_admin_referer( self::POST_CERT );
|
||
|
||
$pem = $this->settings->certificate_pem();
|
||
if ( '' === $pem ) {
|
||
wp_die( esc_html__( 'No certificate is stored.', 'm365-login' ), 404 );
|
||
}
|
||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||
$name = 'm365-login-' . sanitize_file_name( is_string( $host ) ? $host : 'wordpress' ) . '.cer';
|
||
|
||
nocache_headers();
|
||
header( 'Content-Type: application/x-x509-ca-cert' );
|
||
header( 'Content-Disposition: attachment; filename="' . $name . '"' );
|
||
header( 'Content-Length: ' . strlen( $pem ) );
|
||
echo $pem; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PEM text, public certificate only.
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Renders the settings screen.
|
||
*/
|
||
public function render() {
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_die( esc_html__( 'You are not allowed to access this page.', 'm365-login' ) );
|
||
}
|
||
|
||
$s = $this->settings->all();
|
||
$configured = $this->settings->is_configured();
|
||
$has_secret = '' !== $this->settings->client_secret();
|
||
$method = $this->settings->auth_method();
|
||
$cert_info = $this->settings->certificate_info();
|
||
$option = M365_LOGIN_OPTION;
|
||
$field = function ( $key ) use ( $option ) {
|
||
return esc_attr( $option . '[' . $key . ']' );
|
||
};
|
||
?>
|
||
<div class="wrap m365-admin">
|
||
<header class="m365-admin__header">
|
||
<div class="m365-admin__brand">
|
||
<span class="m365-admin__logo"><?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?></span>
|
||
<div>
|
||
<h1><?php esc_html_e( 'M365 Login', 'm365-login' ); ?></h1>
|
||
<p><?php esc_html_e( 'Let existing users sign in with their Microsoft 365 / Entra ID account.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
<span class="m365-admin__status <?php echo $configured ? 'is-ok' : 'is-pending'; ?>">
|
||
<span class="m365-admin__status-dot"></span>
|
||
<?php echo $configured ? esc_html__( 'Connected', 'm365-login' ) : esc_html__( 'Setup incomplete', 'm365-login' ); ?>
|
||
</span>
|
||
</header>
|
||
|
||
<form method="post" action="options.php" class="m365-admin__form" novalidate>
|
||
<?php settings_fields( self::GROUP ); ?>
|
||
|
||
<nav class="m365-admin__tabs" role="tablist">
|
||
<button type="button" class="m365-admin__tab is-active" role="tab" data-tab="connection" aria-selected="true"><?php esc_html_e( 'Connection', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-admin__tab" role="tab" data-tab="button" aria-selected="false"><?php esc_html_e( 'Button', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-admin__tab" role="tab" data-tab="security" aria-selected="false"><?php esc_html_e( 'Security', 'm365-login' ); ?></button>
|
||
</nav>
|
||
|
||
<div class="m365-admin__layout">
|
||
<div class="m365-admin__main">
|
||
|
||
<!-- Connection -->
|
||
<section class="m365-admin__panel is-active" data-panel="connection">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Microsoft Entra ID app registration', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Enter the values from your app registration in the Microsoft Entra admin center.', 'm365-login' ); ?></p>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-tenant"><?php esc_html_e( 'Directory (tenant) ID', 'm365-login' ); ?></label>
|
||
<div class="m365-field__row">
|
||
<input type="text" id="m365-tenant" name="<?php echo $field( 'tenant_id' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['tenant_id'] ); ?>" class="regular-text code" placeholder="00000000-0000-0000-0000-000000000000" autocomplete="off" spellcheck="false" />
|
||
<button type="button" class="button" id="m365-test"><?php esc_html_e( 'Test tenant', 'm365-login' ); ?></button>
|
||
</div>
|
||
<p class="description"><?php esc_html_e( 'Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. "organizations" allows any work or school account.', 'm365-login' ); ?></p>
|
||
<?php if ( $this->settings->is_multi_tenant() && '' !== $s['tenant_id'] ) : ?>
|
||
<p class="m365-warning m365-warning--strong"><?php esc_html_e( 'Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their "email" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
<div id="m365-test-result" class="m365-inline-result" hidden></div>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-client-id"><?php esc_html_e( 'Application (client) ID', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-client-id" name="<?php echo $field( 'client_id' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['client_id'] ); ?>" class="regular-text code" placeholder="00000000-0000-0000-0000-000000000000" autocomplete="off" spellcheck="false" />
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'How should WordPress authenticate to Microsoft?', 'm365-login' ); ?></span>
|
||
<div class="m365-method">
|
||
<label class="m365-method__option" data-method="secret">
|
||
<input type="radio" name="<?php echo $field( 'auth_method' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="secret" <?php checked( $method, 'secret' ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Client secret', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
<label class="m365-method__option" data-method="certificate">
|
||
<input type="radio" name="<?php echo $field( 'auth_method' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="certificate" <?php checked( $method, 'certificate' ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Certificate', 'm365-login' ); ?><span class="m365-method__badge"><?php esc_html_e( 'Recommended', 'm365-login' ); ?></span></strong>
|
||
<em><?php esc_html_e( 'The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Secret -->
|
||
<div class="m365-auth-panel" data-method="secret">
|
||
<div class="m365-field">
|
||
<label for="m365-client-secret"><?php esc_html_e( 'Client secret', 'm365-login' ); ?></label>
|
||
<div class="m365-field__row">
|
||
<input type="password" id="m365-client-secret" name="<?php echo $field( 'client_secret' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="" class="regular-text code" autocomplete="new-password" placeholder="<?php echo $has_secret ? esc_attr__( '•••••••••••• (stored, leave empty to keep)', 'm365-login' ) : esc_attr__( 'Paste the secret value', 'm365-login' ); ?>" />
|
||
<button type="button" class="button m365-toggle-secret" aria-label="<?php esc_attr_e( 'Show secret', 'm365-login' ); ?>"><span class="dashicons dashicons-visibility"></span></button>
|
||
</div>
|
||
<?php if ( $has_secret ) : ?>
|
||
<label class="m365-check m365-check--inline">
|
||
<input type="checkbox" name="<?php echo $field( 'client_secret_clear' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" />
|
||
<?php esc_html_e( 'Remove the stored secret', 'm365-login' ); ?>
|
||
</label>
|
||
<?php endif; ?>
|
||
<p class="description"><?php esc_html_e( 'Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID.', 'm365-login' ); ?></p>
|
||
</div>
|
||
|
||
<details class="m365-guide" <?php echo $has_secret ? '' : 'open'; ?>>
|
||
<summary><?php esc_html_e( 'Step-by-step: create a client secret in Entra ID', 'm365-login' ); ?></summary>
|
||
<div class="m365-guide__body">
|
||
<ol>
|
||
<li><?php esc_html_e( 'Open entra.microsoft.com and sign in with an account that has the "Application Administrator" or "Global Administrator" role.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Enter a description such as "WordPress login" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Paste the value into the Client secret field above and save this page.', 'm365-login' ); ?></li>
|
||
</ol>
|
||
<p class="m365-guide__note"><?php esc_html_e( 'When the secret expires, sign-ins fail with "Could not complete the sign-in with Microsoft". Create a new secret, paste it here, save, then delete the old one in Entra ID.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Certificate -->
|
||
<div class="m365-auth-panel" data-method="certificate">
|
||
<div class="m365-cert">
|
||
<?php if ( $cert_info ) : ?>
|
||
<?php
|
||
$days_left = (int) floor( ( $cert_info['not_after'] - time() ) / DAY_IN_SECONDS );
|
||
if ( $days_left < 0 ) {
|
||
$status_class = 'is-bad';
|
||
$status_text = __( 'Expired', 'm365-login' );
|
||
} elseif ( $days_left < 30 ) {
|
||
$status_class = 'is-warn';
|
||
/* translators: %d: number of days */
|
||
$status_text = sprintf( __( 'Expires in %d days', 'm365-login' ), $days_left );
|
||
} else {
|
||
$status_class = 'is-ok';
|
||
$status_text = __( 'Valid', 'm365-login' );
|
||
}
|
||
?>
|
||
<span class="m365-cert__status <?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_text ); ?></span>
|
||
<dl class="m365-cert__grid">
|
||
<dt><?php esc_html_e( 'Thumbprint (SHA-1)', 'm365-login' ); ?></dt>
|
||
<dd><code id="m365-cert-thumbprint"><?php echo esc_html( $cert_info['thumbprint'] ); ?></code> <button type="button" class="button button-small m365-copy__button" data-copy="m365-cert-thumbprint"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button></dd>
|
||
<dt><?php esc_html_e( 'Subject', 'm365-login' ); ?></dt>
|
||
<dd><?php echo esc_html( $cert_info['subject'] ); ?></dd>
|
||
<dt><?php esc_html_e( 'Key size', 'm365-login' ); ?></dt>
|
||
<dd><?php echo esc_html( $cert_info['bits'] ); ?> Bit RSA</dd>
|
||
<dt><?php esc_html_e( 'Valid until', 'm365-login' ); ?></dt>
|
||
<dd><?php echo esc_html( wp_date( get_option( 'date_format' ), $cert_info['not_after'] ) ); ?></dd>
|
||
</dl>
|
||
<div class="m365-cert__actions">
|
||
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=' . self::POST_CERT ), self::POST_CERT ) ); ?>"><?php esc_html_e( 'Download certificate (.cer)', 'm365-login' ); ?></a>
|
||
<button type="button" class="button" id="m365-cert-generate" data-replace="1"><?php esc_html_e( 'Generate new certificate', 'm365-login' ); ?></button>
|
||
<label class="m365-check m365-check--inline">
|
||
<input type="checkbox" name="<?php echo $field( 'cert_remove' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" id="m365-cert-remove" />
|
||
<?php esc_html_e( 'Remove certificate when saving', 'm365-login' ); ?>
|
||
</label>
|
||
</div>
|
||
<?php else : ?>
|
||
<p class="m365-cert__empty"><?php esc_html_e( 'No certificate stored yet.', 'm365-login' ); ?></p>
|
||
<div class="m365-cert__actions">
|
||
<button type="button" class="button button-primary" id="m365-cert-generate"><?php esc_html_e( 'Generate certificate', 'm365-login' ); ?></button>
|
||
<span class="description"><?php esc_html_e( '3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable.', 'm365-login' ); ?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div id="m365-cert-result" class="m365-inline-result" hidden></div>
|
||
<p class="description" style="margin-top:12px"><a href="#" id="m365-cert-paste-toggle"><?php esc_html_e( 'Use your own certificate instead (paste PEM)', 'm365-login' ); ?></a></p>
|
||
<div id="m365-cert-paste" hidden>
|
||
<div class="m365-field">
|
||
<label for="m365-cert-key"><?php esc_html_e( 'Private key (PEM, unencrypted)', 'm365-login' ); ?></label>
|
||
<textarea id="m365-cert-key" name="<?php echo $field( 'cert_key_pem' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" class="large-text m365-pem" rows="6" placeholder="-----BEGIN PRIVATE KEY-----" autocomplete="off" spellcheck="false"></textarea>
|
||
</div>
|
||
<div class="m365-field">
|
||
<label for="m365-cert-cert"><?php esc_html_e( 'Certificate (PEM)', 'm365-login' ); ?></label>
|
||
<textarea id="m365-cert-cert" name="<?php echo $field( 'cert_cert_pem' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" class="large-text m365-pem" rows="6" placeholder="-----BEGIN CERTIFICATE-----" spellcheck="false"></textarea>
|
||
<p class="description"><?php esc_html_e( 'RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="m365-guide" <?php echo $cert_info ? 'open' : ''; ?>>
|
||
<summary><?php esc_html_e( 'Step-by-step: register the certificate in Entra ID', 'm365-login' ); ?></summary>
|
||
<div class="m365-guide__body">
|
||
<ol>
|
||
<li><?php esc_html_e( 'Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Open entra.microsoft.com → Identity → Applications → App registrations and open your app.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Select the downloaded .cer file, add a description such as "WordPress login" and click Add.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now.', 'm365-login' ); ?></li>
|
||
</ol>
|
||
<p class="m365-guide__note"><?php esc_html_e( 'How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted.', 'm365-login' ); ?></p>
|
||
<p class="m365-guide__warn"><?php esc_html_e( 'Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-prompt"><?php esc_html_e( 'Account prompt', 'm365-login' ); ?></label>
|
||
<select id="m365-prompt" name="<?php echo $field( 'prompt' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
|
||
<option value="select_account" <?php selected( $s['prompt'], 'select_account' ); ?>><?php esc_html_e( 'Always let the user pick an account (recommended)', 'm365-login' ); ?></option>
|
||
<option value="none" <?php selected( $s['prompt'], 'none' ); ?>><?php esc_html_e( 'Use the current Microsoft session if available', 'm365-login' ); ?></option>
|
||
<option value="login" <?php selected( $s['prompt'], 'login' ); ?>><?php esc_html_e( 'Always require re-entering credentials', 'm365-login' ); ?></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Button -->
|
||
<section class="m365-admin__panel" data-panel="button">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Appearance', 'm365-login' ); ?></h2>
|
||
|
||
<div class="m365-preview">
|
||
<span class="m365-preview__label"><?php esc_html_e( 'Live preview', 'm365-login' ); ?></span>
|
||
<div class="m365-preview__stage">
|
||
<div class="m365-login m365-login--preview" id="m365-preview" style="<?php echo esc_attr( str_replace( array( '.m365-login{', '}' ), '', M365_Login::instance()->button->css_variables() ) ); ?>">
|
||
<div class="m365-login__divider"><span id="m365-preview-divider"><?php echo esc_html( $s['divider_text'] ); ?></span></div>
|
||
<a class="m365-login__button" href="#" onclick="return false;" id="m365-preview-button">
|
||
<span id="m365-preview-icon"><?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?></span>
|
||
<span class="m365-login__label" id="m365-preview-text"><?php echo esc_html( $s['button_text'] ); ?></span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-grid">
|
||
<div class="m365-field">
|
||
<label for="m365-button-text"><?php esc_html_e( 'Button text', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-button-text" name="<?php echo $field( 'button_text' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['button_text'] ); ?>" class="regular-text" maxlength="80" data-preview="text" />
|
||
</div>
|
||
<div class="m365-field">
|
||
<label for="m365-divider-text"><?php esc_html_e( 'Divider text', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-divider-text" name="<?php echo $field( 'divider_text' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['divider_text'] ); ?>" class="regular-text" maxlength="40" data-preview="divider" />
|
||
<p class="description"><?php esc_html_e( 'Leave empty to hide the divider line.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Icon', 'm365-login' ); ?></span>
|
||
<label class="m365-check">
|
||
<input type="checkbox" name="<?php echo $field( 'button_show_icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['button_show_icon'] ); ?> data-preview="show-icon" />
|
||
<?php esc_html_e( 'Show an icon on the button', 'm365-login' ); ?>
|
||
</label>
|
||
<div class="m365-icon-picker">
|
||
<div class="m365-icon-picker__thumb" id="m365-icon-thumb">
|
||
<?php if ( '' !== $s['button_icon'] ) : ?>
|
||
<img src="<?php echo esc_url( $s['button_icon'] ); ?>" alt="" />
|
||
<?php else : ?>
|
||
<?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="m365-icon-picker__controls">
|
||
<input type="url" id="m365-icon-url" name="<?php echo $field( 'button_icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_url( $s['button_icon'] ); ?>" class="regular-text code" placeholder="<?php esc_attr_e( 'Default: Microsoft logo', 'm365-login' ); ?>" data-preview="icon" />
|
||
<div class="m365-field__row">
|
||
<button type="button" class="button" id="m365-icon-choose"><?php esc_html_e( 'Choose from media library', 'm365-login' ); ?></button>
|
||
<button type="button" class="button-link m365-link-danger" id="m365-icon-reset"><?php esc_html_e( 'Use Microsoft logo', 'm365-login' ); ?></button>
|
||
</div>
|
||
<p class="description"><?php esc_html_e( 'PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-grid m365-grid--4">
|
||
<?php
|
||
$colors = array(
|
||
'button_bg' => __( 'Background', 'm365-login' ),
|
||
'button_bg_hover' => __( 'Background (hover)', 'm365-login' ),
|
||
'button_color' => __( 'Text colour', 'm365-login' ),
|
||
'button_border' => __( 'Border', 'm365-login' ),
|
||
);
|
||
foreach ( $colors as $key => $label ) :
|
||
?>
|
||
<div class="m365-field">
|
||
<label for="m365-<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label>
|
||
<input type="text" id="m365-<?php echo esc_attr( $key ); ?>" name="<?php echo $field( $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s[ $key ] ); ?>" class="m365-color" data-default-color="<?php echo esc_attr( $this->settings->defaults()[ $key ] ); ?>" data-preview="<?php echo esc_attr( str_replace( 'button_', '', $key ) ); ?>" />
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="m365-grid">
|
||
<div class="m365-field">
|
||
<label for="m365-radius"><?php esc_html_e( 'Corner radius', 'm365-login' ); ?> <span class="m365-range-value" id="m365-radius-value"><?php echo esc_html( $s['button_radius'] ); ?> px</span></label>
|
||
<input type="range" id="m365-radius" name="<?php echo $field( 'button_radius' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['button_radius'] ); ?>" min="0" max="50" step="1" data-preview="radius" />
|
||
</div>
|
||
<div class="m365-field">
|
||
<label for="m365-position"><?php esc_html_e( 'Position on the login page', 'm365-login' ); ?></label>
|
||
<select id="m365-position" name="<?php echo $field( 'button_position' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
|
||
<option value="below" <?php selected( $s['button_position'], 'below' ); ?>><?php esc_html_e( 'Below the login form', 'm365-login' ); ?></option>
|
||
<option value="above" <?php selected( $s['button_position'], 'above' ); ?>><?php esc_html_e( 'Above the login form', 'm365-login' ); ?></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-presets">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Quick presets', 'm365-login' ); ?></span>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#2f2f2f","bg_hover":"#1a1a1a","color":"#ffffff","border":"#2f2f2f"}'><span style="background:#2f2f2f"></span><?php esc_html_e( 'Microsoft dark', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#ffffff","bg_hover":"#f3f3f3","color":"#5e5e5e","border":"#8c8c8c"}'><span style="background:#ffffff;border-color:#8c8c8c"></span><?php esc_html_e( 'Microsoft light', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#0078d4","bg_hover":"#106ebe","color":"#ffffff","border":"#0078d4"}'><span style="background:#0078d4"></span><?php esc_html_e( 'Azure blue', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#2271b1","bg_hover":"#135e96","color":"#ffffff","border":"#2271b1"}'><span style="background:#2271b1"></span><?php esc_html_e( 'WordPress blue', 'm365-login' ); ?></button>
|
||
</div>
|
||
</div>
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Custom login page', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there.', 'm365-login' ); ?></p>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-custom-login"><?php esc_html_e( 'URL of your login page', 'm365-login' ); ?></label>
|
||
<input type="url" id="m365-custom-login" name="<?php echo $field( 'custom_login_url' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_url( $s['custom_login_url'] ); ?>" class="regular-text code" placeholder="<?php echo esc_attr( home_url( '/login/' ) ); ?>" />
|
||
<p class="description"><?php esc_html_e( 'Must be on this site. Leave empty to use wp-login.php.', 'm365-login' ); ?></p>
|
||
</div>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'inject_form' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['inject_form'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Add the button to every wp_login_form() form automatically', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Manual placement', 'm365-login' ); ?></span>
|
||
<p class="description"><?php esc_html_e( 'Shortcode (block editor, page builders):', 'm365-login' ); ?></p>
|
||
<code>[m365_login_button redirect="/dashboard/" divider="yes"]</code>
|
||
<p class="description"><?php esc_html_e( 'Template function (theme files):', 'm365-login' ); ?></p>
|
||
<code><?php m365_login_button( array( 'redirect' => '/dashboard/' ) ); ?></code>
|
||
<p class="description"><?php esc_html_e( 'Both show the error messages of the last attempt; use m365_login_messages() to place them separately.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Security -->
|
||
<section class="m365-admin__panel" data-panel="security">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'User matching & hardening', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists.', 'm365-login' ); ?></p>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'bind_oid' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['bind_oid'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Bind WordPress accounts to the Microsoft object ID', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'upn_fallback' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['upn_fallback'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Fall back to the user principal name (UPN)', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'If the token contains no "email" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'remember_me' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['remember_me'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Keep users signed in ("Remember me")', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Issues a 14-day WordPress session instead of a browser session.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-domains"><?php esc_html_e( 'Allowed e-mail domains (optional)', 'm365-login' ); ?></label>
|
||
<textarea id="m365-domains" name="<?php echo $field( 'allowed_domains' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" rows="3" class="large-text code" placeholder="contoso.com, contoso.de"><?php echo esc_textarea( $s['allowed_domains'] ); ?></textarea>
|
||
<p class="description"><?php esc_html_e( 'One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant.', 'm365-login' ); ?></p>
|
||
</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( 'Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI 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 and allows password sign-in there. 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">
|
||
<li><?php esc_html_e( 'OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Client secret encrypted at rest; no accounts are created, no passwords are changed.', 'm365-login' ); ?></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="m365-admin__actions">
|
||
<?php submit_button( __( 'Save changes', 'm365-login' ), 'primary large', 'submit', false ); ?>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="m365-admin__sidebar">
|
||
<div class="m365-card m365-card--accent">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Redirect URI', 'm365-login' ); ?></h2>
|
||
<p><?php esc_html_e( 'Register this URI in your app registration under Authentication → Web → Redirect URIs:', 'm365-login' ); ?></p>
|
||
<div class="m365-copy">
|
||
<code id="m365-redirect-uri"><?php echo esc_html( $this->settings->redirect_uri() ); ?></code>
|
||
<button type="button" class="button button-small m365-copy__button" data-copy="m365-redirect-uri"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button>
|
||
</div>
|
||
<?php if ( ! $this->settings->uses_pretty_callback() ) : ?>
|
||
<p class="description"><?php esc_html_e( 'Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
<?php if ( ! is_ssl() && 'https' !== wp_parse_url( home_url(), PHP_URL_SCHEME ) ) : ?>
|
||
<p class="m365-warning"><?php esc_html_e( 'Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Setup guide: app registration', 'm365-login' ); ?></h2>
|
||
<ol class="m365-steps">
|
||
<li><?php esc_html_e( 'Open entra.microsoft.com → Identity → Applications → App registrations → New registration.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Name: e.g. "WordPress login". Supported account types: "Accounts in this organizational directory only" (single tenant).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Redirect URI: choose the platform Web and paste the URI shown above. Then click Register.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Authentication: leave "ID tokens" unchecked (the plugin uses the authorization code flow) and "Allow public client flows" on No.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Token configuration → Add optional claim → ID → tick "email" → Add. Confirm the API permission prompt.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Optional: restrict who may use the app under Enterprise applications → your app → Properties → "Assignment required" = Yes, then assign users/groups.', '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">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Shortcode', 'm365-login' ); ?></h2>
|
||
<p><?php esc_html_e( 'Place the button on a custom login page:', 'm365-login' ); ?></p>
|
||
<code>[m365_login_button redirect="/my-account/"]</code>
|
||
<p class="description"><?php esc_html_e( 'More options on the Button tab under "Custom login page".', 'm365-login' ); ?></p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|