diff --git a/.github/assets/button-preview.svg b/.github/assets/button-preview.svg new file mode 100644 index 0000000..c898145 --- /dev/null +++ b/.github/assets/button-preview.svg @@ -0,0 +1,64 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cae79b..ad01554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,5 +13,7 @@ All notable changes to this project are documented in this file. The format foll - ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce). - Encrypted client secret storage (AES-256-GCM). - Account binding to the Microsoft object ID, e-mail domain allow-list. +- Entra group restriction with a Graph-powered group picker; membership verified via the `groups` claim or Microsoft Graph `checkMemberGroups`. +- Button-only mode that hides the password form and blocks password sign-in on `wp-login.php`, with a secret, rate-limited fallback link and a `wp-config.php` emergency constant. - `[m365_login_button]` shortcode and developer hooks. - German translation. diff --git a/README.md b/README.md index e42a70d..4628664 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,394 @@ -# M365 Login für WordPress +
m365_login_allow_user nicht nachrüsten – das wäre ein anderes Sicherheitsmodell.
+consumers oder common stellen. Microsoft erlaubt dann keine Query-Strings in Redirect-URIs, deshalb müssen sprechende Permalinks aktiv sein (Callback ohne ?).
+define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true ); in die wp-config.php oder den Plugin-Ordner per FTP umbenennen.
+' ).text( id ) );
+ $li.append( $( '' ).attr( 'name', optionName + '[allowed_groups][' + id + ']' ).val( name || id ) );
+ $li.append( $( '' ) );
+ $groupList.append( $li );
+ refreshGroupList();
+ }
+
+ $groupList.on( 'click', '.m365-group-chip__remove', function () {
+ $( this ).closest( 'li' ).remove();
+ refreshGroupList();
+ } );
+ refreshGroupList();
+
+ function searchGroups() {
+ var query = $.trim( $( '#m365-group-search' ).val() );
+ $groupResults.prop( 'hidden', false ).removeClass( 'is-error' ).html( '' + escapeHtml( i18n.searching ) + '
' );
+
+ $.post( cfg.ajaxUrl, {
+ action: cfg.groupAction,
+ nonce: cfg.nonce,
+ query: query
+ } ).done( function ( res ) {
+ if ( ! res || ! res.success ) {
+ var msg = ( res && res.data && res.data.message ) || i18n.testFailed;
+ $groupResults.addClass( 'is-error' ).html( '' + escapeHtml( msg ) + '
' );
+ // Allow adding a pasted GUID even when Graph is unavailable.
+ if ( /^[0-9a-f-]{36}$/i.test( query ) ) {
+ $groupResults.append( buildResult( { id: query, name: query, type: '', description: '' } ) );
+ }
+ return;
+ }
+ var groups = res.data.groups || [];
+ if ( ! groups.length ) {
+ $groupResults.html( '' + escapeHtml( i18n.noGroups ) + '
' );
+ return;
+ }
+ $groupResults.empty();
+ $.each( groups, function ( i, g ) {
+ $groupResults.append( buildResult( g ) );
+ } );
+ } ).fail( function () {
+ $groupResults.addClass( 'is-error' ).html( '' + escapeHtml( i18n.testFailed ) + '
' );
+ } );
+ }
+
+ function buildResult( g ) {
+ var $row = $( '' );
+ var $meta = $( '
+
+
+
+
+
+
+
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings->allowed_groups() as $gid => $gname ) : ?>
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings->fallback_url() ) : ?>
+
+ settings->fallback_url() ); ?>
+
+
+
+
+
+
+
+ define( \'M365_LOGIN_DISABLE_BUTTON_ONLY\', true );'
+ );
+ ?>
+
+
+
+
@@ -444,6 +562,7 @@ class M365_Login_Admin {
+
diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php
index f3d40fe..2efa8c6 100644
--- a/includes/class-m365-login-auth.php
+++ b/includes/class-m365-login-auth.php
@@ -15,6 +15,8 @@ class M365_Login_Auth {
const ACTION_START = 'm365_login';
const CALLBACK_PATH = 'm365-login/callback';
const STATE_COOKIE = 'm365_login_state';
+ const FALLBACK_COOKIE = 'm365_login_fallback';
+ const FALLBACK_TTL = 30 * MINUTE_IN_SECONDS;
const STATE_TTL = 600; // 10 minutes.
const META_OID = '_m365_login_oid';
const META_LAST_LOGIN = '_m365_login_last_login';
@@ -28,17 +30,123 @@ class M365_Login_Auth {
*/
private $settings;
+ /**
+ * Graph client.
+ *
+ * @var M365_Login_Graph
+ */
+ private $graph;
+
/**
* Constructor.
*
* @param M365_Login_Settings $settings Settings.
+ * @param M365_Login_Graph $graph Graph client.
*/
- public function __construct( M365_Login_Settings $settings ) {
+ public function __construct( M365_Login_Settings $settings, M365_Login_Graph $graph ) {
$this->settings = $settings;
+ $this->graph = $graph;
add_action( 'login_form_' . self::ACTION_START, array( $this, 'handle_start' ) );
add_action( 'init', array( $this, 'maybe_handle_callback' ), 5 );
add_filter( 'wp_login_errors', array( $this, 'login_errors' ), 10, 1 );
+
+ // Button-only mode.
+ add_action( 'login_init', array( $this, 'maybe_accept_fallback_key' ) );
+ // Runs after core's username/password handlers (priority 20), which would otherwise overwrite an early WP_Error.
+ add_filter( 'authenticate', array( $this, 'block_password_login' ), 99, 3 );
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Button-only mode */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Whether the current browser presented the fallback key (cookie set for 30 minutes).
+ *
+ * @return bool
+ */
+ public function fallback_active() {
+ if ( ! $this->settings->button_only() ) {
+ return true; // Nothing is hidden, the form is always available.
+ }
+ $cookie = isset( $_COOKIE[ self::FALLBACK_COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::FALLBACK_COOKIE ] ) ) : '';
+ return '' !== $cookie && hash_equals( $this->fallback_cookie_value(), $cookie );
+ }
+
+ /**
+ * Expected fallback cookie value (HMAC of the key, so the key itself never sits in the cookie).
+ *
+ * @return string
+ */
+ private function fallback_cookie_value() {
+ return hash_hmac( 'sha256', 'fallback|' . $this->settings->fallback_key(), wp_salt( 'auth' ) );
+ }
+
+ /**
+ * wp-login.php?m365_fallback=KEY → sets the fallback cookie and reloads without the key in the URL.
+ */
+ public function maybe_accept_fallback_key() {
+ if ( ! $this->settings->button_only() ) {
+ return;
+ }
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the key itself is the secret.
+ $given = isset( $_GET['m365_fallback'] ) ? sanitize_text_field( wp_unslash( $_GET['m365_fallback'] ) ) : '';
+ if ( '' === $given ) {
+ return;
+ }
+
+ // Slow down brute force attempts on the key.
+ $ip_key = 'm365_login_fb_' . md5( $this->client_ip() );
+ $attempts = (int) get_transient( $ip_key );
+ if ( $attempts >= 10 ) {
+ $this->fail( 'fallback_locked' );
+ }
+
+ if ( ! hash_equals( $this->settings->fallback_key(), $given ) ) {
+ set_transient( $ip_key, $attempts + 1, 15 * MINUTE_IN_SECONDS );
+ $this->fail( 'fallback_invalid' );
+ }
+
+ delete_transient( $ip_key );
+ $this->send_cookie( self::FALLBACK_COOKIE, $this->fallback_cookie_value(), time() + self::FALLBACK_TTL );
+ nocache_headers();
+ wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', wp_login_url() ) );
+ exit;
+ }
+
+ /**
+ * Refuses username/password sign-in on wp-login.php while button-only mode is active.
+ *
+ * @param null|WP_User|WP_Error $user Result so far.
+ * @param string $username Username.
+ * @param string $password Password.
+ * @return null|WP_User|WP_Error
+ */
+ public function block_password_login( $user, $username, $password ) {
+ if ( ! $this->settings->button_only() || $this->fallback_active() ) {
+ return $user;
+ }
+ if ( '' === (string) $username && '' === (string) $password ) {
+ return $user; // Initial form render or cookie auth, no password attempt.
+ }
+ // Only the interactive login form is affected: XML-RPC, REST and application passwords keep working.
+ if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) {
+ return $user;
+ }
+ if ( ! $user instanceof WP_User ) {
+ return $user; // Already failed for another reason; keep core's message.
+ }
+ return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) );
+ }
+
+ /**
+ * Best-effort client IP for rate limiting.
+ *
+ * @return string
+ */
+ private function client_ip() {
+ return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0';
}
/* ------------------------------------------------------------------ */
@@ -276,8 +384,15 @@ class M365_Login_Auth {
$this->fail( 'no_user' );
}
- // Bind the account to the immutable Microsoft object ID after first login.
$oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : '';
+
+ // Entra group restriction.
+ $group_check = $this->check_groups( $claims, $oid );
+ if ( true !== $group_check ) {
+ $this->fail( $group_check );
+ }
+
+ // Bind the account to the immutable Microsoft object ID after first login.
if ( $this->settings->get( 'bind_oid' ) ) {
if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) {
$this->fail( 'invalid_token' );
@@ -459,6 +574,49 @@ class M365_Login_Auth {
return $body;
}
+ /**
+ * Verifies membership in one of the allowed Entra groups.
+ *
+ * Uses the "groups" claim when the token carries one (and is not in overage),
+ * otherwise asks Microsoft Graph (transitive check, needs application permissions).
+ *
+ * @param array $claims Verified claims.
+ * @param string $oid User object ID.
+ * @return true|string True, or an error code for fail().
+ */
+ private function check_groups( $claims, $oid ) {
+ $allowed = array_keys( $this->settings->allowed_groups() );
+ if ( empty( $allowed ) ) {
+ return true;
+ }
+
+ $overage = ! empty( $claims['_claim_names'] ) || ! empty( $claims['hasgroups'] );
+ if ( ! $overage && isset( $claims['groups'] ) && is_array( $claims['groups'] ) ) {
+ $token_groups = array_map( 'strtolower', array_filter( $claims['groups'], 'is_string' ) );
+ if ( array_intersect( $allowed, $token_groups ) ) {
+ return true;
+ }
+ // The claim is authoritative when present: no need to ask Graph.
+ $this->log( 'User is not a member of an allowed group (token claim).' );
+ return 'not_in_group';
+ }
+
+ if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) {
+ return 'invalid_token';
+ }
+
+ $matches = $this->graph->check_member_groups( $oid, $allowed );
+ if ( is_wp_error( $matches ) ) {
+ $this->log( 'Group check via Microsoft Graph failed: ' . $matches->get_error_message() );
+ return 'group_check_failed';
+ }
+ if ( empty( $matches ) ) {
+ $this->log( 'User is not a member of an allowed group (Graph).' );
+ return 'not_in_group';
+ }
+ return true;
+ }
+
/**
* Extracts the e-mail address used for matching.
*
@@ -520,28 +678,29 @@ class M365_Login_Auth {
* @param string $token Cookie value.
*/
private function set_state_cookie( $token ) {
- $this->send_cookie( $token, time() + self::STATE_TTL );
+ $this->send_cookie( self::STATE_COOKIE, $token, time() + self::STATE_TTL );
}
/**
* Removes the state cookie.
*/
private function clear_state_cookie() {
- $this->send_cookie( '', time() - YEAR_IN_SECONDS );
+ $this->send_cookie( self::STATE_COOKIE, '', time() - YEAR_IN_SECONDS );
}
/**
* Cookie helper: HttpOnly, SameSite=Lax (needed for the top-level redirect back), Secure on HTTPS.
*
+ * @param string $name Cookie name.
* @param string $value Value.
* @param int $expires Expiry timestamp.
*/
- private function send_cookie( $value, $expires ) {
+ private function send_cookie( $name, $value, $expires ) {
$path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
$path = is_string( $path ) && '' !== $path ? $path : '/';
setcookie(
- self::STATE_COOKIE,
+ $name,
$value,
array(
'expires' => $expires,
@@ -583,8 +742,18 @@ class M365_Login_Auth {
* @return WP_Error
*/
public function login_errors( $errors ) {
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display of a whitelisted error code.
- $code = isset( $_GET['m365_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_error'] ) ) : '';
+ if ( ! $errors instanceof WP_Error ) {
+ $errors = new WP_Error();
+ }
+
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only display of whitelisted flags.
+ $code = isset( $_GET['m365_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_error'] ) ) : '';
+ $fallback_on = isset( $_GET['m365_fallback'] ) && 'on' === $_GET['m365_fallback'];
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
+
+ if ( $fallback_on && $this->settings->button_only() && $this->fallback_active() ) {
+ $errors->add( 'm365_login_fallback_on', __( 'Password sign-in is temporarily enabled for this browser (30 minutes).', 'm365-login' ), 'message' );
+ }
if ( '' === $code ) {
return $errors;
}
@@ -601,11 +770,12 @@ class M365_Login_Auth {
'no_user' => __( 'No WordPress account exists for your Microsoft e-mail address.', 'm365-login' ),
'oid_mismatch' => __( 'This WordPress account is linked to a different Microsoft account. Please contact an administrator.', 'm365-login' ),
'not_allowed' => __( 'You are not allowed to sign in with this account.', 'm365-login' ),
+ 'not_in_group' => __( 'Your Microsoft account is not a member of a group that is allowed to sign in here.', 'm365-login' ),
+ 'group_check_failed' => __( 'Your group membership could not be verified. Please contact an administrator.', 'm365-login' ),
+ 'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
+ 'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
);
- if ( ! $errors instanceof WP_Error ) {
- $errors = new WP_Error();
- }
$errors->add(
'm365_login_' . $code,
isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],
diff --git a/includes/class-m365-login-button.php b/includes/class-m365-login-button.php
index e378ae6..5ff81f8 100644
--- a/includes/class-m365-login-button.php
+++ b/includes/class-m365-login-button.php
@@ -28,6 +28,7 @@ class M365_Login_Button {
$this->settings = $settings;
add_action( 'login_enqueue_scripts', array( $this, 'enqueue' ) );
+ add_filter( 'login_body_class', array( $this, 'body_class' ) );
add_filter( 'login_message', array( $this, 'render_above' ), 20 );
add_action( 'login_footer', array( $this, 'render_below' ) );
add_shortcode( 'm365_login_button', array( $this, 'shortcode' ) );
@@ -57,6 +58,28 @@ class M365_Login_Button {
return (bool) apply_filters( 'm365_login_show_button', true );
}
+ /**
+ * Whether the password form is hidden for this request.
+ *
+ * @return bool
+ */
+ private function form_hidden() {
+ return $this->should_render() && $this->settings->button_only() && ! M365_Login::instance()->auth->fallback_active();
+ }
+
+ /**
+ * Adds a body class while the password form is hidden.
+ *
+ * @param string[] $classes Body classes.
+ * @return string[]
+ */
+ public function body_class( $classes ) {
+ if ( $this->form_hidden() ) {
+ $classes[] = 'm365-button-only';
+ }
+ return $classes;
+ }
+
/**
* Enqueues login styles and the small positioning script.
*/
@@ -140,7 +163,7 @@ class M365_Login_Button {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- passed through to the flow, validated there.
$redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : '';
- $divider = (string) $this->settings->get( 'divider_text' );
+ $divider = $this->form_hidden() ? '' : (string) $this->settings->get( 'divider_text' );
$divider = '' === trim( $divider ) ? '' : '';
$html = '';
diff --git a/includes/class-m365-login-graph.php b/includes/class-m365-login-graph.php
new file mode 100644
index 0000000..28bfb4d
--- /dev/null
+++ b/includes/class-m365-login-graph.php
@@ -0,0 +1,240 @@
+settings = $settings;
+ }
+
+ /**
+ * Transient key for the cached app token.
+ *
+ * @return string
+ */
+ private function token_cache_key() {
+ return 'm365_login_apptoken_' . md5( $this->settings->tenant() . '|' . $this->settings->get( 'client_id' ) );
+ }
+
+ /**
+ * Drops the cached app token (e.g. after the client secret changed).
+ */
+ public function flush_token() {
+ delete_transient( $this->token_cache_key() );
+ }
+
+ /**
+ * Returns an app-only access token for Microsoft Graph.
+ *
+ * @return string|WP_Error
+ */
+ public function app_token() {
+ $cached = get_transient( $this->token_cache_key() );
+ if ( is_string( $cached ) && '' !== $cached ) {
+ return $cached;
+ }
+
+ if ( ! $this->settings->is_configured() ) {
+ return new WP_Error( 'graph_not_configured', __( 'Microsoft login is not configured yet.', 'm365-login' ) );
+ }
+
+ $response = wp_remote_post(
+ 'https://login.microsoftonline.com/' . rawurlencode( $this->settings->tenant() ) . '/oauth2/v2.0/token',
+ array(
+ 'timeout' => self::HTTP_TIMEOUT,
+ 'headers' => array( 'Accept' => 'application/json' ),
+ 'body' => array(
+ 'client_id' => $this->settings->get( 'client_id' ),
+ 'client_secret' => $this->settings->client_secret(),
+ 'grant_type' => 'client_credentials',
+ 'scope' => 'https://graph.microsoft.com/.default',
+ ),
+ )
+ );
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $body = json_decode( wp_remote_retrieve_body( $response ), true );
+ if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) || empty( $body['access_token'] ) ) {
+ $detail = is_array( $body ) && ! empty( $body['error_description'] ) ? (string) $body['error_description'] : 'HTTP ' . wp_remote_retrieve_response_code( $response );
+ return new WP_Error( 'graph_token', $detail );
+ }
+
+ set_transient( $this->token_cache_key(), (string) $body['access_token'], self::TOKEN_TTL );
+ return (string) $body['access_token'];
+ }
+
+ /**
+ * Performs an authenticated Graph request.
+ *
+ * @param string $method HTTP method.
+ * @param string $path Path relative to the v1.0 base (with query string).
+ * @param array|null $json JSON body for POST requests.
+ * @param array $headers Extra headers.
+ * @return array|WP_Error Decoded JSON.
+ */
+ private function request( $method, $path, $json = null, $headers = array() ) {
+ $token = $this->app_token();
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $args = array(
+ 'method' => $method,
+ 'timeout' => self::HTTP_TIMEOUT,
+ 'headers' => array_merge(
+ array(
+ 'Authorization' => 'Bearer ' . $token,
+ 'Accept' => 'application/json',
+ ),
+ $headers
+ ),
+ );
+ if ( null !== $json ) {
+ $args['headers']['Content-Type'] = 'application/json';
+ $args['body'] = wp_json_encode( $json );
+ }
+
+ $response = wp_remote_request( self::GRAPH_BASE . $path, $args );
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $code = (int) wp_remote_retrieve_response_code( $response );
+ $body = json_decode( wp_remote_retrieve_body( $response ), true );
+
+ if ( 401 === $code ) {
+ $this->flush_token();
+ }
+ if ( $code < 200 || $code >= 300 || ! is_array( $body ) ) {
+ $graph_code = isset( $body['error']['code'] ) ? (string) $body['error']['code'] : 'HTTP ' . $code;
+ $message = isset( $body['error']['message'] ) ? (string) $body['error']['message'] : '';
+ return new WP_Error( 'graph_' . sanitize_key( $graph_code ), $graph_code . ( $message ? ': ' . $message : '' ) );
+ }
+
+ return $body;
+ }
+
+ /**
+ * Searches groups by display name.
+ *
+ * @param string $query Search text (may be empty for the first page).
+ * @return array|WP_Error List of ['id' => .., 'name' => .., 'description' => ..].
+ */
+ public function search_groups( $query ) {
+ $query = trim( (string) $query );
+ $select = '$select=id,displayName,description,securityEnabled,mailEnabled&$top=25&$orderby=displayName';
+
+ if ( '' !== $query && M365_Login_Settings::is_guid( $query ) ) {
+ $path = '/groups/' . rawurlencode( strtolower( $query ) ) . '?$select=id,displayName,description,securityEnabled,mailEnabled';
+ $item = $this->request( 'GET', $path );
+ if ( is_wp_error( $item ) ) {
+ return $item;
+ }
+ return array( $this->format_group( $item ) );
+ }
+
+ $path = '/groups?' . $select;
+ if ( '' !== $query ) {
+ // $search needs the ConsistencyLevel header; the value must be wrapped in double quotes.
+ $search = '"displayName:' . str_replace( '"', '', $query ) . '"';
+ $path = '/groups?' . $select . '&$search=' . rawurlencode( $search ) . '&$count=true';
+ }
+
+ $result = $this->request( 'GET', $path, null, array( 'ConsistencyLevel' => 'eventual' ) );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ $groups = array();
+ if ( ! empty( $result['value'] ) && is_array( $result['value'] ) ) {
+ foreach ( $result['value'] as $item ) {
+ if ( is_array( $item ) && ! empty( $item['id'] ) ) {
+ $groups[] = $this->format_group( $item );
+ }
+ }
+ }
+ return $groups;
+ }
+
+ /**
+ * Normalises a Graph group object.
+ *
+ * @param array $item Graph group.
+ * @return array
+ */
+ private function format_group( $item ) {
+ $type = __( 'Group', 'm365-login' );
+ if ( ! empty( $item['securityEnabled'] ) && empty( $item['mailEnabled'] ) ) {
+ $type = __( 'Security group', 'm365-login' );
+ } elseif ( ! empty( $item['mailEnabled'] ) ) {
+ $type = __( 'Microsoft 365 group', 'm365-login' );
+ }
+ return array(
+ 'id' => strtolower( (string) $item['id'] ),
+ 'name' => isset( $item['displayName'] ) ? (string) $item['displayName'] : (string) $item['id'],
+ 'description' => isset( $item['description'] ) ? (string) $item['description'] : '',
+ 'type' => $type,
+ );
+ }
+
+ /**
+ * Checks (transitively) which of the given groups the user belongs to.
+ *
+ * @param string $user_oid User object ID.
+ * @param string[] $group_ids Group object IDs (any count; chunked by 20).
+ * @return string[]|WP_Error Matching group IDs.
+ */
+ public function check_member_groups( $user_oid, $group_ids ) {
+ if ( ! M365_Login_Settings::is_guid( $user_oid ) ) {
+ return new WP_Error( 'graph_bad_oid', 'Invalid user object ID.' );
+ }
+
+ $matches = array();
+ foreach ( array_chunk( array_values( $group_ids ), 20 ) as $chunk ) {
+ $result = $this->request(
+ 'POST',
+ '/users/' . rawurlencode( strtolower( $user_oid ) ) . '/checkMemberGroups',
+ array( 'groupIds' => $chunk )
+ );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ if ( ! empty( $result['value'] ) && is_array( $result['value'] ) ) {
+ foreach ( $result['value'] as $id ) {
+ $matches[] = strtolower( (string) $id );
+ }
+ }
+ if ( ! empty( $matches ) ) {
+ break; // One match is enough.
+ }
+ }
+ return $matches;
+ }
+}
diff --git a/includes/class-m365-login-settings.php b/includes/class-m365-login-settings.php
index 872746b..bf78c22 100644
--- a/includes/class-m365-login-settings.php
+++ b/includes/class-m365-login-settings.php
@@ -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 );
diff --git a/includes/class-m365-login.php b/includes/class-m365-login.php
index 530731c..78d9c7b 100644
--- a/includes/class-m365-login.php
+++ b/includes/class-m365-login.php
@@ -33,6 +33,13 @@ final class M365_Login {
*/
public $auth;
+ /**
+ * Microsoft Graph client.
+ *
+ * @var M365_Login_Graph
+ */
+ public $graph;
+
/**
* Login button component.
*
@@ -66,11 +73,12 @@ final class M365_Login {
add_action( 'init', array( $this, 'load_textdomain' ) );
$this->settings = new M365_Login_Settings();
- $this->auth = new M365_Login_Auth( $this->settings );
+ $this->graph = new M365_Login_Graph( $this->settings );
+ $this->auth = new M365_Login_Auth( $this->settings, $this->graph );
$this->button = new M365_Login_Button( $this->settings );
if ( is_admin() ) {
- $this->admin = new M365_Login_Admin( $this->settings, $this->auth );
+ $this->admin = new M365_Login_Admin( $this->settings, $this->auth, $this->graph );
}
add_filter( 'plugin_action_links_' . plugin_basename( M365_LOGIN_FILE ), array( $this, 'action_links' ) );
diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo
index b8b9f05..e0634a8 100644
Binary files a/languages/m365-login-de_DE.mo and b/languages/m365-login-de_DE.mo differ
diff --git a/languages/m365-login-de_DE.po b/languages/m365-login-de_DE.po
index 00c5ac6..1f61c79 100644
--- a/languages/m365-login-de_DE.po
+++ b/languages/m365-login-de_DE.po
@@ -16,445 +16,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203
+#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:102
+#: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
-#: includes/class-m365-login-admin.php:104
+#: includes/class-m365-login-admin.php:116
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:132
+#: includes/class-m365-login-admin.php:145
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:133
+#: includes/class-m365-login-admin.php:146
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:134
+#: includes/class-m365-login-admin.php:147
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:136
+#: includes/class-m365-login-admin.php:149
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:137
+#: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:149
+#: includes/class-m365-login-admin.php:151
+msgid "No groups found."
+msgstr "Keine Gruppen gefunden."
+
+#: includes/class-m365-login-admin.php:152
+msgid "Searching…"
+msgstr "Suche läuft …"
+
+#: includes/class-m365-login-admin.php:153
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+msgid "Remove"
+msgstr "Entfernen"
+
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+msgid "Save the connection settings first, then search for groups."
+msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
+
+#: includes/class-m365-login-admin.php:156
+msgid "Generate a new fallback key on save? The old link stops working."
+msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
+
+#: includes/class-m365-login-admin.php:168 includes/class-m365-login-admin.php:207
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:154
+#: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:168
+#: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:177
+#: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:187
+#: includes/class-m365-login-admin.php:219
+msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
+msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
+
+#: includes/class-m365-login-admin.php:232
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:204
+#: includes/class-m365-login-admin.php:249
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:209
+#: includes/class-m365-login-admin.php:254
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:209
+#: includes/class-m365-login-admin.php:254
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:217
+#: includes/class-m365-login-admin.php:262
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:218
+#: includes/class-m365-login-admin.php:263
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:219
+#: includes/class-m365-login-admin.php:264
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:228
+#: includes/class-m365-login-admin.php:273
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:229
+#: includes/class-m365-login-admin.php:274
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Trage hier die Werte aus deiner App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:232
+#: includes/class-m365-login-admin.php:277
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:235
+#: includes/class-m365-login-admin.php:280
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:237
+#: includes/class-m365-login-admin.php:282
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID deines Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:242
+#: includes/class-m365-login-admin.php:287
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:247
+#: includes/class-m365-login-admin.php:292
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:249
+#: includes/class-m365-login-admin.php:294
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:249
+#: includes/class-m365-login-admin.php:294
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:250
+#: includes/class-m365-login-admin.php:295
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:255
+#: includes/class-m365-login-admin.php:300
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:258
+#: includes/class-m365-login-admin.php:303
msgid "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."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:262
+#: includes/class-m365-login-admin.php:307
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:264
+#: includes/class-m365-login-admin.php:309
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:265
+#: includes/class-m365-login-admin.php:310
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:266
+#: includes/class-m365-login-admin.php:311
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:275
+#: includes/class-m365-login-admin.php:320
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:278
+#: includes/class-m365-login-admin.php:323
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:292
+#: includes/class-m365-login-admin.php:337
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:296
+#: includes/class-m365-login-admin.php:341
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:298
+#: includes/class-m365-login-admin.php:343
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:303
+#: includes/class-m365-login-admin.php:348
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:306
+#: includes/class-m365-login-admin.php:351
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:317
+#: includes/class-m365-login-admin.php:362
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:319
+#: includes/class-m365-login-admin.php:364
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:320
+#: includes/class-m365-login-admin.php:365
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:322
+#: includes/class-m365-login-admin.php:367
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:330
+#: includes/class-m365-login-admin.php:375
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:331
+#: includes/class-m365-login-admin.php:376
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:332
+#: includes/class-m365-login-admin.php:377
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:333
+#: includes/class-m365-login-admin.php:378
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:346
+#: includes/class-m365-login-admin.php:391
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:350
+#: includes/class-m365-login-admin.php:395
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:352
+#: includes/class-m365-login-admin.php:397
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:353
+#: includes/class-m365-login-admin.php:398
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:359
+#: includes/class-m365-login-admin.php:404
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:360
+#: includes/class-m365-login-admin.php:405
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:361
+#: includes/class-m365-login-admin.php:406
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:362
+#: includes/class-m365-login-admin.php:407
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:363
+#: includes/class-m365-login-admin.php:408
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:371
+#: includes/class-m365-login-admin.php:416
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:372
+#: includes/class-m365-login-admin.php:417
msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
-#: includes/class-m365-login-admin.php:377
+#: includes/class-m365-login-admin.php:422
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:378
+#: includes/class-m365-login-admin.php:423
msgid "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."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:385
+#: includes/class-m365-login-admin.php:430
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:386
+#: includes/class-m365-login-admin.php:431
msgid "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."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:438
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:439
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:399
+#: includes/class-m365-login-admin.php:444
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:401
+#: includes/class-m365-login-admin.php:446
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:406
+#: includes/class-m365-login-admin.php:451
+msgid "Allowed Entra groups (optional)"
+msgstr "Erlaubte Entra-Gruppen (optional)"
+
+#: includes/class-m365-login-admin.php:452
+msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
+msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
+
+#: includes/class-m365-login-admin.php:455
+msgid "Search groups"
+msgstr "Gruppen suchen"
+
+#: includes/class-m365-login-admin.php:457
+msgid "Type a group name or paste an object ID…"
+msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
+
+#: includes/class-m365-login-admin.php:458
+msgid "Search"
+msgstr "Suchen"
+
+#: includes/class-m365-login-admin.php:463
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
+
+#: includes/class-m365-login-admin.php:469
+msgid "Selected groups"
+msgstr "Ausgewählte Gruppen"
+
+#: includes/class-m365-login-admin.php:470
+msgid "No groups selected – every matched user may sign in."
+msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
+
+#: includes/class-m365-login-admin.php:480
+msgid "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."
+msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
+
+#: includes/class-m365-login-admin.php:485
+msgid "Button-only mode"
+msgstr "Nur-Button-Modus"
+
+#: includes/class-m365-login-admin.php:486
+msgid "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."
+msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
+
+#: includes/class-m365-login-admin.php:491
+msgid "Show only the Microsoft button on the login page"
+msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
+
+#: includes/class-m365-login-admin.php:492
+msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
+msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
+
+#: includes/class-m365-login-admin.php:497
+msgid "Fallback link (keep it secret)"
+msgstr "Fallback-Link (geheim halten)"
+
+#: includes/class-m365-login-admin.php:498
+msgid "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."
+msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
+
+#: includes/class-m365-login-admin.php:506
+msgid "Generate a new key when saving"
+msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
+
+#: includes/class-m365-login-admin.php:509
+msgid "A key is generated automatically the first time you save these settings."
+msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:515
+msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
+msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:524
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:408
+#: includes/class-m365-login-admin.php:526
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:409
+#: includes/class-m365-login-admin.php:527
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:410
+#: includes/class-m365-login-admin.php:528
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:529
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:535
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:423
+#: includes/class-m365-login-admin.php:541
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:424
+#: includes/class-m365-login-admin.php:542
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:430
+#: includes/class-m365-login-admin.php:548
msgid "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."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:433
+#: includes/class-m365-login-admin.php:551
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:556
msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten"
-#: includes/class-m365-login-admin.php:440
+#: includes/class-m365-login-admin.php:558
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
-#: includes/class-m365-login-admin.php:441
+#: includes/class-m365-login-admin.php:559
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen."
-#: includes/class-m365-login-admin.php:442
+#: includes/class-m365-login-admin.php:560
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
-#: includes/class-m365-login-admin.php:443
+#: includes/class-m365-login-admin.php:561
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:562
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
-#: includes/class-m365-login-admin.php:446
+#: includes/class-m365-login-admin.php:564
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:450
+#: includes/class-m365-login-admin.php:565
+msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
+
+#: includes/class-m365-login-admin.php:569
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:451
+#: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-auth.php:593
+#: includes/class-m365-login-auth.php:136
+msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
+msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
+
+#: includes/class-m365-login-auth.php:751
+msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
+msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
+
+#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:594
+#: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:595
+#: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:596
+#: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:597
+#: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:598
+#: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:599
+#: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:600
+#: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:601
+#: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:602
+#: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:603
+#: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-settings.php:40
+#: includes/class-m365-login-auth.php:769
+msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
+msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
+
+#: includes/class-m365-login-auth.php:770
+msgid "Your group membership could not be verified. Please contact an administrator."
+msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
+
+#: includes/class-m365-login-auth.php:771
+msgid "The fallback key is not valid."
+msgstr "Der Fallback-Schlüssel ist ungültig."
+
+#: includes/class-m365-login-auth.php:772
+msgid "Too many attempts. Please wait 15 minutes."
+msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
+
+#: includes/class-m365-login-graph.php:193
+msgid "Group"
+msgstr "Gruppe"
+
+#: includes/class-m365-login-graph.php:195
+msgid "Security group"
+msgstr "Sicherheitsgruppe"
+
+#: includes/class-m365-login-graph.php:197
+msgid "Microsoft 365 group"
+msgstr "Microsoft 365-Gruppe"
+
+#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
-#: includes/class-m365-login-settings.php:49
+#: includes/class-m365-login-settings.php:53
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:176
+#: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:184
+#: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:196
+#: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:200
+#: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login.php:94
+#: includes/class-m365-login.php:102
msgid "Settings"
msgstr "Einstellungen"
-#: includes/class-m365-login.php:105
+#: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer."
-#: includes/class-m365-login.php:106 includes/class-m365-login.php:115
+#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen"
-#: includes/class-m365-login.php:114
+#: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."
diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo
index 6276c82..4c79e3d 100644
Binary files a/languages/m365-login-de_DE_formal.mo and b/languages/m365-login-de_DE_formal.mo differ
diff --git a/languages/m365-login-de_DE_formal.po b/languages/m365-login-de_DE_formal.po
index 742f086..ed19b16 100644
--- a/languages/m365-login-de_DE_formal.po
+++ b/languages/m365-login-de_DE_formal.po
@@ -16,445 +16,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203
+#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:102
+#: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
-#: includes/class-m365-login-admin.php:104
+#: includes/class-m365-login-admin.php:116
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:132
+#: includes/class-m365-login-admin.php:145
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:133
+#: includes/class-m365-login-admin.php:146
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:134
+#: includes/class-m365-login-admin.php:147
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:136
+#: includes/class-m365-login-admin.php:149
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:137
+#: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:149
+#: includes/class-m365-login-admin.php:151
+msgid "No groups found."
+msgstr "Keine Gruppen gefunden."
+
+#: includes/class-m365-login-admin.php:152
+msgid "Searching…"
+msgstr "Suche läuft …"
+
+#: includes/class-m365-login-admin.php:153
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+msgid "Remove"
+msgstr "Entfernen"
+
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+msgid "Save the connection settings first, then search for groups."
+msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
+
+#: includes/class-m365-login-admin.php:156
+msgid "Generate a new fallback key on save? The old link stops working."
+msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
+
+#: includes/class-m365-login-admin.php:168 includes/class-m365-login-admin.php:207
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:154
+#: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:168
+#: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:177
+#: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:187
+#: includes/class-m365-login-admin.php:219
+msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
+msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
+
+#: includes/class-m365-login-admin.php:232
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:204
+#: includes/class-m365-login-admin.php:249
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:209
+#: includes/class-m365-login-admin.php:254
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:209
+#: includes/class-m365-login-admin.php:254
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:217
+#: includes/class-m365-login-admin.php:262
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:218
+#: includes/class-m365-login-admin.php:263
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:219
+#: includes/class-m365-login-admin.php:264
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:228
+#: includes/class-m365-login-admin.php:273
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:229
+#: includes/class-m365-login-admin.php:274
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Tragen Sie hier die Werte aus Ihrer App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:232
+#: includes/class-m365-login-admin.php:277
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:235
+#: includes/class-m365-login-admin.php:280
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:237
+#: includes/class-m365-login-admin.php:282
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID Ihres Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:242
+#: includes/class-m365-login-admin.php:287
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:247
+#: includes/class-m365-login-admin.php:292
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:249
+#: includes/class-m365-login-admin.php:294
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:249
+#: includes/class-m365-login-admin.php:294
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:250
+#: includes/class-m365-login-admin.php:295
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:255
+#: includes/class-m365-login-admin.php:300
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:258
+#: includes/class-m365-login-admin.php:303
msgid "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."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:262
+#: includes/class-m365-login-admin.php:307
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:264
+#: includes/class-m365-login-admin.php:309
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:265
+#: includes/class-m365-login-admin.php:310
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:266
+#: includes/class-m365-login-admin.php:311
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:275
+#: includes/class-m365-login-admin.php:320
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:278
+#: includes/class-m365-login-admin.php:323
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:292
+#: includes/class-m365-login-admin.php:337
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:296
+#: includes/class-m365-login-admin.php:341
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:298
+#: includes/class-m365-login-admin.php:343
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:303
+#: includes/class-m365-login-admin.php:348
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:306
+#: includes/class-m365-login-admin.php:351
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:317
+#: includes/class-m365-login-admin.php:362
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:319
+#: includes/class-m365-login-admin.php:364
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:320
+#: includes/class-m365-login-admin.php:365
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:322
+#: includes/class-m365-login-admin.php:367
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:330
+#: includes/class-m365-login-admin.php:375
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:331
+#: includes/class-m365-login-admin.php:376
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:332
+#: includes/class-m365-login-admin.php:377
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:333
+#: includes/class-m365-login-admin.php:378
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:346
+#: includes/class-m365-login-admin.php:391
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:350
+#: includes/class-m365-login-admin.php:395
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:352
+#: includes/class-m365-login-admin.php:397
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:353
+#: includes/class-m365-login-admin.php:398
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:359
+#: includes/class-m365-login-admin.php:404
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:360
+#: includes/class-m365-login-admin.php:405
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:361
+#: includes/class-m365-login-admin.php:406
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:362
+#: includes/class-m365-login-admin.php:407
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:363
+#: includes/class-m365-login-admin.php:408
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:371
+#: includes/class-m365-login-admin.php:416
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:372
+#: includes/class-m365-login-admin.php:417
msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
-#: includes/class-m365-login-admin.php:377
+#: includes/class-m365-login-admin.php:422
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:378
+#: includes/class-m365-login-admin.php:423
msgid "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."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:385
+#: includes/class-m365-login-admin.php:430
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:386
+#: includes/class-m365-login-admin.php:431
msgid "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."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:438
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:439
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:399
+#: includes/class-m365-login-admin.php:444
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:401
+#: includes/class-m365-login-admin.php:446
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:406
+#: includes/class-m365-login-admin.php:451
+msgid "Allowed Entra groups (optional)"
+msgstr "Erlaubte Entra-Gruppen (optional)"
+
+#: includes/class-m365-login-admin.php:452
+msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
+msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
+
+#: includes/class-m365-login-admin.php:455
+msgid "Search groups"
+msgstr "Gruppen suchen"
+
+#: includes/class-m365-login-admin.php:457
+msgid "Type a group name or paste an object ID…"
+msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
+
+#: includes/class-m365-login-admin.php:458
+msgid "Search"
+msgstr "Suchen"
+
+#: includes/class-m365-login-admin.php:463
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
+
+#: includes/class-m365-login-admin.php:469
+msgid "Selected groups"
+msgstr "Ausgewählte Gruppen"
+
+#: includes/class-m365-login-admin.php:470
+msgid "No groups selected – every matched user may sign in."
+msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
+
+#: includes/class-m365-login-admin.php:480
+msgid "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."
+msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
+
+#: includes/class-m365-login-admin.php:485
+msgid "Button-only mode"
+msgstr "Nur-Button-Modus"
+
+#: includes/class-m365-login-admin.php:486
+msgid "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."
+msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
+
+#: includes/class-m365-login-admin.php:491
+msgid "Show only the Microsoft button on the login page"
+msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
+
+#: includes/class-m365-login-admin.php:492
+msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
+msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
+
+#: includes/class-m365-login-admin.php:497
+msgid "Fallback link (keep it secret)"
+msgstr "Fallback-Link (geheim halten)"
+
+#: includes/class-m365-login-admin.php:498
+msgid "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."
+msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
+
+#: includes/class-m365-login-admin.php:506
+msgid "Generate a new key when saving"
+msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
+
+#: includes/class-m365-login-admin.php:509
+msgid "A key is generated automatically the first time you save these settings."
+msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:515
+msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
+msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:524
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:408
+#: includes/class-m365-login-admin.php:526
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:409
+#: includes/class-m365-login-admin.php:527
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:410
+#: includes/class-m365-login-admin.php:528
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:529
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:535
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:423
+#: includes/class-m365-login-admin.php:541
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:424
+#: includes/class-m365-login-admin.php:542
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:430
+#: includes/class-m365-login-admin.php:548
msgid "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."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:433
+#: includes/class-m365-login-admin.php:551
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:556
msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten"
-#: includes/class-m365-login-admin.php:440
+#: includes/class-m365-login-admin.php:558
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
-#: includes/class-m365-login-admin.php:441
+#: includes/class-m365-login-admin.php:559
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen."
-#: includes/class-m365-login-admin.php:442
+#: includes/class-m365-login-admin.php:560
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
-#: includes/class-m365-login-admin.php:443
+#: includes/class-m365-login-admin.php:561
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:562
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
-#: includes/class-m365-login-admin.php:446
+#: includes/class-m365-login-admin.php:564
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:450
+#: includes/class-m365-login-admin.php:565
+msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
+
+#: includes/class-m365-login-admin.php:569
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:451
+#: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-auth.php:593
+#: includes/class-m365-login-auth.php:136
+msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
+msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
+
+#: includes/class-m365-login-auth.php:751
+msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
+msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
+
+#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:594
+#: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:595
+#: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:596
+#: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:597
+#: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:598
+#: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:599
+#: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:600
+#: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:601
+#: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:602
+#: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:603
+#: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-settings.php:40
+#: includes/class-m365-login-auth.php:769
+msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
+msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
+
+#: includes/class-m365-login-auth.php:770
+msgid "Your group membership could not be verified. Please contact an administrator."
+msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
+
+#: includes/class-m365-login-auth.php:771
+msgid "The fallback key is not valid."
+msgstr "Der Fallback-Schlüssel ist ungültig."
+
+#: includes/class-m365-login-auth.php:772
+msgid "Too many attempts. Please wait 15 minutes."
+msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
+
+#: includes/class-m365-login-graph.php:193
+msgid "Group"
+msgstr "Gruppe"
+
+#: includes/class-m365-login-graph.php:195
+msgid "Security group"
+msgstr "Sicherheitsgruppe"
+
+#: includes/class-m365-login-graph.php:197
+msgid "Microsoft 365 group"
+msgstr "Microsoft 365-Gruppe"
+
+#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
-#: includes/class-m365-login-settings.php:49
+#: includes/class-m365-login-settings.php:53
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:176
+#: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:184
+#: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:196
+#: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:200
+#: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login.php:94
+#: includes/class-m365-login.php:102
msgid "Settings"
msgstr "Einstellungen"
-#: includes/class-m365-login.php:105
+#: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer."
-#: includes/class-m365-login.php:106 includes/class-m365-login.php:115
+#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen"
-#: includes/class-m365-login.php:114
+#: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."
diff --git a/languages/m365-login.pot b/languages/m365-login.pot
index 8680749..b5bce91 100644
--- a/languages/m365-login.pot
+++ b/languages/m365-login.pot
@@ -14,445 +14,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203
+#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login"
msgstr ""
-#: includes/class-m365-login-admin.php:102
+#: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr ""
-#: includes/class-m365-login-admin.php:104
+#: includes/class-m365-login-admin.php:116
msgid "Open the settings"
msgstr ""
-#: includes/class-m365-login-admin.php:132
+#: includes/class-m365-login-admin.php:145
msgid "Choose button icon"
msgstr ""
-#: includes/class-m365-login-admin.php:133
+#: includes/class-m365-login-admin.php:146
msgid "Use this icon"
msgstr ""
-#: includes/class-m365-login-admin.php:134
+#: includes/class-m365-login-admin.php:147
msgid "Copied!"
msgstr ""
-#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy"
msgstr ""
-#: includes/class-m365-login-admin.php:136
+#: includes/class-m365-login-admin.php:149
msgid "Testing…"
msgstr ""
-#: includes/class-m365-login-admin.php:137
+#: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr ""
-#: includes/class-m365-login-admin.php:149
+#: includes/class-m365-login-admin.php:151
+msgid "No groups found."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:152
+msgid "Searching…"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:153
+msgid "Add"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+msgid "Remove"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+msgid "Save the connection settings first, then search for groups."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:156
+msgid "Generate a new fallback key on save? The old link stops working."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:168 includes/class-m365-login-admin.php:207
msgid "You are not allowed to do this."
msgstr ""
-#: includes/class-m365-login-admin.php:154
+#: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first."
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:168
+#: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:177
+#: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr ""
-#: includes/class-m365-login-admin.php:187
-msgid "You are not allowed to access this page."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:204
-msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:209
-msgid "Connected"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:209
-msgid "Setup incomplete"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:217
-msgid "Connection"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:218
-msgid "Button"
-msgstr ""
-
#: includes/class-m365-login-admin.php:219
-msgid "Security"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:228
-msgid "Microsoft Entra ID app registration"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:229
-msgid "Enter the values from your app registration in the Microsoft Entra admin center."
+msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr ""
#: includes/class-m365-login-admin.php:232
-msgid "Directory (tenant) ID"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:235
-msgid "Test tenant"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:237
-msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:242
-msgid "Application (client) ID"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:247
-msgid "Client secret"
+msgid "You are not allowed to access this page."
msgstr ""
#: includes/class-m365-login-admin.php:249
-msgid "•••••••••••• (stored, leave empty to keep)"
+msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr ""
-#: includes/class-m365-login-admin.php:249
-msgid "Paste the secret value"
+#: includes/class-m365-login-admin.php:254
+msgid "Connected"
msgstr ""
-#: includes/class-m365-login-admin.php:250
-msgid "Show secret"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:255
-msgid "Remove the stored secret"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:258
-msgid "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."
+#: includes/class-m365-login-admin.php:254
+msgid "Setup incomplete"
msgstr ""
#: includes/class-m365-login-admin.php:262
-msgid "Account prompt"
+msgid "Connection"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:263
+msgid "Button"
msgstr ""
#: includes/class-m365-login-admin.php:264
-msgid "Always let the user pick an account (recommended)"
+msgid "Security"
msgstr ""
-#: includes/class-m365-login-admin.php:265
-msgid "Use the current Microsoft session if available"
+#: includes/class-m365-login-admin.php:273
+msgid "Microsoft Entra ID app registration"
msgstr ""
-#: includes/class-m365-login-admin.php:266
-msgid "Always require re-entering credentials"
+#: includes/class-m365-login-admin.php:274
+msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr ""
-#: includes/class-m365-login-admin.php:275
-msgid "Appearance"
+#: includes/class-m365-login-admin.php:277
+msgid "Directory (tenant) ID"
msgstr ""
-#: includes/class-m365-login-admin.php:278
-msgid "Live preview"
+#: includes/class-m365-login-admin.php:280
+msgid "Test tenant"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:282
+msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:287
+msgid "Application (client) ID"
msgstr ""
#: includes/class-m365-login-admin.php:292
-msgid "Button text"
+msgid "Client secret"
msgstr ""
-#: includes/class-m365-login-admin.php:296
-msgid "Divider text"
+#: includes/class-m365-login-admin.php:294
+msgid "•••••••••••• (stored, leave empty to keep)"
msgstr ""
-#: includes/class-m365-login-admin.php:298
-msgid "Leave empty to hide the divider line."
+#: includes/class-m365-login-admin.php:294
+msgid "Paste the secret value"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:295
+msgid "Show secret"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:300
+msgid "Remove the stored secret"
msgstr ""
#: includes/class-m365-login-admin.php:303
-msgid "Icon"
+msgid "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."
msgstr ""
-#: includes/class-m365-login-admin.php:306
-msgid "Show an icon on the button"
+#: includes/class-m365-login-admin.php:307
+msgid "Account prompt"
msgstr ""
-#: includes/class-m365-login-admin.php:317
-msgid "Default: Microsoft logo"
+#: includes/class-m365-login-admin.php:309
+msgid "Always let the user pick an account (recommended)"
msgstr ""
-#: includes/class-m365-login-admin.php:319
-msgid "Choose from media library"
+#: includes/class-m365-login-admin.php:310
+msgid "Use the current Microsoft session if available"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:311
+msgid "Always require re-entering credentials"
msgstr ""
#: includes/class-m365-login-admin.php:320
-msgid "Use Microsoft logo"
+msgid "Appearance"
msgstr ""
-#: includes/class-m365-login-admin.php:322
-msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
+#: includes/class-m365-login-admin.php:323
+msgid "Live preview"
msgstr ""
-#: includes/class-m365-login-admin.php:330
-msgid "Background"
+#: includes/class-m365-login-admin.php:337
+msgid "Button text"
msgstr ""
-#: includes/class-m365-login-admin.php:331
-msgid "Background (hover)"
+#: includes/class-m365-login-admin.php:341
+msgid "Divider text"
msgstr ""
-#: includes/class-m365-login-admin.php:332
-msgid "Text colour"
+#: includes/class-m365-login-admin.php:343
+msgid "Leave empty to hide the divider line."
msgstr ""
-#: includes/class-m365-login-admin.php:333
-msgid "Border"
+#: includes/class-m365-login-admin.php:348
+msgid "Icon"
msgstr ""
-#: includes/class-m365-login-admin.php:346
-msgid "Corner radius"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:350
-msgid "Position on the login page"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:352
-msgid "Below the login form"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:353
-msgid "Above the login form"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:359
-msgid "Quick presets"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:360
-msgid "Microsoft dark"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:361
-msgid "Microsoft light"
+#: includes/class-m365-login-admin.php:351
+msgid "Show an icon on the button"
msgstr ""
#: includes/class-m365-login-admin.php:362
-msgid "Azure blue"
+msgid "Default: Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:363
-msgid "WordPress blue"
+#: includes/class-m365-login-admin.php:364
+msgid "Choose from media library"
msgstr ""
-#: includes/class-m365-login-admin.php:371
-msgid "User matching & hardening"
+#: includes/class-m365-login-admin.php:365
+msgid "Use Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:372
-msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
+#: includes/class-m365-login-admin.php:367
+msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:375
+msgid "Background"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:376
+msgid "Background (hover)"
msgstr ""
#: includes/class-m365-login-admin.php:377
-msgid "Bind WordPress accounts to the Microsoft object ID"
+msgid "Text colour"
msgstr ""
#: includes/class-m365-login-admin.php:378
-msgid "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."
+msgid "Border"
msgstr ""
-#: includes/class-m365-login-admin.php:385
-msgid "Fall back to the user principal name (UPN)"
+#: includes/class-m365-login-admin.php:391
+msgid "Corner radius"
msgstr ""
-#: includes/class-m365-login-admin.php:386
-msgid "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."
+#: includes/class-m365-login-admin.php:395
+msgid "Position on the login page"
msgstr ""
-#: includes/class-m365-login-admin.php:393
-msgid "Keep users signed in (\"Remember me\")"
+#: includes/class-m365-login-admin.php:397
+msgid "Below the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:394
-msgid "Issues a 14-day WordPress session instead of a browser session."
+#: includes/class-m365-login-admin.php:398
+msgid "Above the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:399
-msgid "Allowed e-mail domains (optional)"
+#: includes/class-m365-login-admin.php:404
+msgid "Quick presets"
msgstr ""
-#: includes/class-m365-login-admin.php:401
-msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
+#: includes/class-m365-login-admin.php:405
+msgid "Microsoft dark"
msgstr ""
#: includes/class-m365-login-admin.php:406
-msgid "What the plugin does to keep sign-ins safe"
+msgid "Microsoft light"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:407
+msgid "Azure blue"
msgstr ""
#: includes/class-m365-login-admin.php:408
-msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
+msgid "WordPress blue"
msgstr ""
-#: includes/class-m365-login-admin.php:409
-msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:410
-msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:411
-msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
+#: includes/class-m365-login-admin.php:416
+msgid "User matching & hardening"
msgstr ""
#: includes/class-m365-login-admin.php:417
-msgid "Save changes"
+msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:422
+msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr ""
#: includes/class-m365-login-admin.php:423
-msgid "Redirect URI"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:424
-msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
+msgid "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."
msgstr ""
#: includes/class-m365-login-admin.php:430
-msgid "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."
+msgid "Fall back to the user principal name (UPN)"
msgstr ""
-#: includes/class-m365-login-admin.php:433
-msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
+#: includes/class-m365-login-admin.php:431
+msgid "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."
msgstr ""
#: includes/class-m365-login-admin.php:438
-msgid "Setup in 5 steps"
+msgid "Keep users signed in (\"Remember me\")"
msgstr ""
-#: includes/class-m365-login-admin.php:440
-msgid "Open the Microsoft Entra admin center → App registrations → New registration."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:441
-msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:442
-msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:443
-msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
+#: includes/class-m365-login-admin.php:439
+msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr ""
#: includes/class-m365-login-admin.php:444
-msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
+msgid "Allowed e-mail domains (optional)"
msgstr ""
#: includes/class-m365-login-admin.php:446
-msgid "Required API permission: openid, profile, email (delegated) – granted by default."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:450
-msgid "Shortcode"
+msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr ""
#: includes/class-m365-login-admin.php:451
+msgid "Allowed Entra groups (optional)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:452
+msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:455
+msgid "Search groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:457
+msgid "Type a group name or paste an object ID…"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:458
+msgid "Search"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:463
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:469
+msgid "Selected groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:470
+msgid "No groups selected – every matched user may sign in."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:480
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:485
+msgid "Button-only mode"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:486
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:491
+msgid "Show only the Microsoft button on the login page"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:492
+msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:497
+msgid "Fallback link (keep it secret)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:498
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:506
+msgid "Generate a new key when saving"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:509
+msgid "A key is generated automatically the first time you save these settings."
+msgstr ""
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:515
+msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
+msgstr ""
+
+#. translators: %s: PHP constant
+#: includes/class-m365-login-admin.php:524
+msgid "What the plugin does to keep sign-ins safe"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:526
+msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:527
+msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:528
+msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:529
+msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:535
+msgid "Save changes"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:541
+msgid "Redirect URI"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:542
+msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:548
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:551
+msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:556
+msgid "Setup in 5 steps"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:558
+msgid "Open the Microsoft Entra admin center → App registrations → New registration."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:559
+msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:560
+msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:561
+msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:562
+msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:564
+msgid "Required API permission: openid, profile, email (delegated) – granted by default."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:565
+msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:569
+msgid "Shortcode"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:"
msgstr ""
-#: includes/class-m365-login-auth.php:593
+#: includes/class-m365-login-auth.php:136
+msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:751
+msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr ""
-#: includes/class-m365-login-auth.php:594
+#: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:595
+#: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled."
msgstr ""
-#: includes/class-m365-login-auth.php:596
+#: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:597
+#: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:598
+#: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified."
msgstr ""
-#: includes/class-m365-login-auth.php:599
+#: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:600
+#: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:601
+#: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:602
+#: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:603
+#: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account."
msgstr ""
-#: includes/class-m365-login-settings.php:40
+#: includes/class-m365-login-auth.php:769
+msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:770
+msgid "Your group membership could not be verified. Please contact an administrator."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:771
+msgid "The fallback key is not valid."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:772
+msgid "Too many attempts. Please wait 15 minutes."
+msgstr ""
+
+#: includes/class-m365-login-graph.php:193
+msgid "Group"
+msgstr ""
+
+#: includes/class-m365-login-graph.php:195
+msgid "Security group"
+msgstr ""
+
+#: includes/class-m365-login-graph.php:197
+msgid "Microsoft 365 group"
+msgstr ""
+
+#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft"
msgstr ""
-#: includes/class-m365-login-settings.php:49
+#: includes/class-m365-login-settings.php:53
msgid "or"
msgstr ""
-#: includes/class-m365-login-settings.php:176
+#: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr ""
-#: includes/class-m365-login-settings.php:184
+#: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID."
msgstr ""
-#: includes/class-m365-login-settings.php:196
+#: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters."
msgstr ""
-#: includes/class-m365-login-settings.php:200
+#: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
-#: includes/class-m365-login.php:94
+#: includes/class-m365-login.php:102
msgid "Settings"
msgstr ""
-#: includes/class-m365-login.php:105
+#: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer."
msgstr ""
-#: includes/class-m365-login.php:106 includes/class-m365-login.php:115
+#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed"
msgstr ""
-#: includes/class-m365-login.php:114
+#: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr ""
diff --git a/m365-login.php b/m365-login.php
index 19b35f6..190d698 100644
--- a/m365-login.php
+++ b/m365-login.php
@@ -25,6 +25,7 @@ define( 'M365_LOGIN_OPTION', 'm365_login_settings' );
require_once M365_LOGIN_DIR . 'includes/class-m365-login-settings.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-crypto.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-jwt.php';
+require_once M365_LOGIN_DIR . 'includes/class-m365-login-graph.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-auth.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-admin.php';
diff --git a/readme.txt b/readme.txt
index db95119..3b4a65b 100644
--- a/readme.txt
+++ b/readme.txt
@@ -19,6 +19,8 @@ The plugin is deliberately small and strict:
* **No user provisioning.** A Microsoft sign-in succeeds only when a WordPress user with the same e-mail address already exists. Nobody gets an account just by having a Microsoft login.
* **Password login stays available.** The button is an additional option; the normal form is untouched.
* **Fully customisable button.** Change the text, replace the Microsoft logo with your own icon from the media library, pick background, hover, text and border colours, adjust the corner radius, and choose whether the button appears above or below the login form – with a live preview.
+* **Entra group restriction.** Search and pick the groups whose members may sign in, right in the settings screen. Membership is checked via the ID token's `groups` claim or Microsoft Graph (nested groups included).
+* **Button-only mode.** Hide the username/password form and refuse password sign-ins on the login page. A secret fallback link (and a `wp-config.php` constant) brings the form back when you need it.
* **Clean settings screen** with a copy-and-paste redirect URI, a tenant connectivity test and a five-step setup guide.
* **Shortcode** `[m365_login_button]` for custom login pages.
@@ -29,7 +31,8 @@ The plugin is deliberately small and strict:
* The **ID token signature is verified** against Microsoft's published signing keys (JWKS, cached and refreshed on key rollover). Issuer, audience, tenant, expiry, not-before and nonce are all checked. Only RS256 is accepted.
* Optional **tenant pinning**: when a tenant GUID is configured, tokens from any other tenant are rejected.
* **Account binding**: 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 refused.
-* Optional **e-mail domain allow-list**.
+* Optional **e-mail domain allow-list** and **group allow-list** (fails closed when membership cannot be verified).
+* **Button-only mode** blocks password sign-in server-side, not just visually; the fallback key is rate limited and never stored in a cookie.
* The **client secret is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
* Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked.
@@ -52,6 +55,11 @@ Endpoints used (all under `https://login.microsoftonline.com/`):
* `/{tenant}/discovery/v2.0/keys` – the server downloads Microsoft's public signing keys to verify the ID token. No user data is sent.
* `/{tenant}/v2.0/.well-known/openid-configuration` – fetched only when an administrator clicks "Test tenant". No user data is sent.
+When the optional **group restriction** is configured, the plugin additionally connects to **Microsoft Graph** (`https://graph.microsoft.com/v1.0/`) using an application token obtained from `/{tenant}/oauth2/v2.0/token` (client credentials, client ID and secret are sent):
+
+* `/groups` – only when an administrator searches for groups in the settings screen. The typed search text is sent.
+* `/users/{id}/checkMemberGroups` – during sign-in when the ID token carries no usable `groups` claim. The user's Microsoft object ID and the configured group IDs are sent; Microsoft returns which of those groups the user belongs to.
+
The plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID from Microsoft and uses them solely to find the matching WordPress account. Nothing else is stored.
Microsoft terms and privacy: [Microsoft Services Agreement](https://www.microsoft.com/servicesagreement), [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement), [Microsoft identity platform documentation](https://learn.microsoft.com/entra/identity-platform/).
@@ -86,6 +94,14 @@ Set the tenant to `consumers` or `common`. Note that Microsoft does not allow qu
The e-mail address in the Microsoft token does not match any WordPress user. Check the user's e-mail address in WordPress, enable the UPN fallback on the Security tab, or add the `email` optional claim in the app registration.
+= How do I restrict sign-in to certain Entra groups? =
+
+Open the Security tab, search for the groups (requires the Microsoft Graph application permission `GroupMember.Read.All` with admin consent) or paste their object IDs, add them and save. During sign-in the plugin first looks at the `groups` claim of the ID token; if the app registration does not emit one (or the user is in more than 200 groups) it asks Microsoft Graph (`User.Read.All`). If membership cannot be verified, the sign-in is refused.
+
+= How do I get back in when button-only mode is on and Microsoft sign-in is broken? =
+
+Open the fallback link shown on the Security tab (`wp-login.php?m365_fallback=KEY`); the password form is shown again in that browser for 30 minutes. Without the link, add `define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true );` to `wp-config.php` or rename the plugin folder via FTP.
+
= Does it work with custom login pages? =
Yes, use the shortcode `[m365_login_button redirect="/dashboard/"]`.
@@ -103,7 +119,8 @@ The settings, cached data and the per-user Microsoft object ID are removed.
1. The customised button on the WordPress login screen.
2. Settings – Connection tab with redirect URI and tenant test.
3. Settings – Button tab with live preview, colour pickers and icon picker.
-4. Settings – Security tab.
+4. Settings – Security tab with the Entra group picker and button-only mode.
+5. Login screen in button-only mode.
== Changelog ==