Adds a WordPress plugin that places a customisable "Sign in with Microsoft" button on wp-login.php and signs existing users in via the OpenID Connect authorization code flow with PKCE. Users are matched by e-mail address only; no accounts are created. Security: single-use state/nonce bound to an HttpOnly cookie, ID token signature verification against Microsoft's JWKS (RS256 only) with issuer/audience/tenant/expiry/nonce checks, optional tenant pinning, account binding to the Microsoft object ID, e-mail domain allow-list, client secret encrypted at rest (AES-256-GCM). Admin: settings screen with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect URI copy button and tenant connectivity test. Packaging for WordPress.org: readme.txt with External services section, GPL-2.0 license, uninstall.php, POT + German translations, .distignore, build script, PHPCS config and CI running Plugin Check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
460 lines
25 KiB
PHP
460 lines
25 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 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;
|
||
|
||
/**
|
||
* Screen hook suffix.
|
||
*
|
||
* @var string
|
||
*/
|
||
private $hook = '';
|
||
|
||
/**
|
||
* Constructor.
|
||
*
|
||
* @param M365_Login_Settings $settings Settings.
|
||
* @param M365_Login_Auth $auth Auth.
|
||
*/
|
||
public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth ) {
|
||
$this->settings = $settings;
|
||
$this->auth = $auth;
|
||
|
||
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( '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,
|
||
'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' ),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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' ),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
$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>
|
||
<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">
|
||
<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>
|
||
|
||
<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>
|
||
</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 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 in 5 steps', 'm365-login' ); ?></h2>
|
||
<ol class="m365-steps">
|
||
<li><?php esc_html_e( 'Open the Microsoft Entra admin center → App registrations → New registration.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Choose "Accounts in this organizational directory only", set the platform to Web and paste the redirect URI above.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Copy the Application (client) ID and Directory (tenant) ID from the overview page.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Under Certificates & secrets create a client secret and copy its value (not the ID).', 'm365-login' ); ?></li>
|
||
<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>
|
||
</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>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|