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:
friloo 2026-09-22 14:30:53 +00:00
parent 1517e7e3bc
commit 1202283eda
No known key found for this signature in database
20 changed files with 2241 additions and 517 deletions

View file

@ -35,7 +35,11 @@ class M365_Login_Settings {
'upn_fallback' => 1,
'bind_oid' => 1,
'allowed_domains' => '',
'allowed_groups' => array(), // id => display name.
'remember_me' => 0,
// Button-only mode.
'button_only' => 0,
'fallback_key' => '',
// Button appearance.
'button_text' => __( 'Sign in with Microsoft', 'm365-login' ),
'button_icon' => '', // Empty = bundled Microsoft logo.
@ -157,6 +161,71 @@ class M365_Login_Settings {
return array_values( array_unique( $out ) );
}
/**
* Allowed Entra group IDs (lowercase GUIDs) mapped to display names.
*
* @return array
*/
public function allowed_groups() {
$raw = $this->get( 'allowed_groups', array() );
$out = array();
if ( is_array( $raw ) ) {
foreach ( $raw as $id => $name ) {
$id = strtolower( (string) $id );
if ( self::is_guid( $id ) ) {
$out[ $id ] = (string) $name;
}
}
}
return $out;
}
/**
* Whether the password form is hidden and password sign-in blocked.
*
* @return bool
*/
public function button_only() {
if ( defined( 'M365_LOGIN_DISABLE_BUTTON_ONLY' ) && M365_LOGIN_DISABLE_BUTTON_ONLY ) {
return false;
}
return $this->is_configured() && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
}
/**
* Secret key that re-enables the password form.
*
* @return string
*/
public function fallback_key() {
$key = (string) $this->get( 'fallback_key', '' );
return preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ? $key : '';
}
/**
* URL that shows the password form again when button-only mode is active.
*
* @return string
*/
public function fallback_url() {
$key = $this->fallback_key();
return '' === $key ? '' : add_query_arg( 'm365_fallback', $key, wp_login_url() );
}
/**
* Generates a new fallback key.
*
* @return string
*/
public static function generate_fallback_key() {
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
$key = '';
for ( $i = 0; $i < 24; $i++ ) {
$key .= $alphabet[ random_int( 0, strlen( $alphabet ) - 1 ) ];
}
return $key;
}
/**
* Sanitises settings coming from the admin form.
*
@ -215,6 +284,31 @@ class M365_Login_Settings {
$domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains );
$out['allowed_domains'] = trim( (string) $domains );
// Allowed groups: GUID => name.
$groups = array();
if ( ! empty( $input['allowed_groups'] ) && is_array( $input['allowed_groups'] ) ) {
foreach ( $input['allowed_groups'] as $id => $name ) {
$id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
if ( ! self::is_guid( $id ) ) {
continue;
}
$name = sanitize_text_field( wp_unslash( (string) $name ) );
$groups[ $id ] = '' === $name ? $id : mb_substr( $name, 0, 120 );
if ( count( $groups ) >= 100 ) {
break;
}
}
}
$out['allowed_groups'] = $groups;
// Button-only mode + fallback key.
$out['button_only'] = empty( $input['button_only'] ) ? 0 : 1;
$key = (string) $current['fallback_key'];
if ( ! empty( $input['fallback_regenerate'] ) || ! preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ) {
$key = self::generate_fallback_key();
}
$out['fallback_key'] = $key;
// Button.
$text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : '';
$out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 );