Support custom login pages
- Add the button (and error messages) to every wp_login_form() form via login_form_top/login_form_bottom; in button-only mode the password fields are wrapped and hidden there. - New template functions m365_login_button() and m365_login_messages(); the shortcode gains divider and messages attributes. - New setting for the custom login page URL: failed sign-ins, the fallback link and the logout redirect point there instead of wp-login.php. Must be a same-site URL. - Button-only mode now blocks every interactive password sign-in through the authenticate filter, not only wp-login.php; XML-RPC, REST, WP-CLI and cron are exempt, plus a filter for trusted exceptions. - Fallback key accepted on any page (init) instead of login_init only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
This commit is contained in:
parent
e5db0d49be
commit
701e85ab88
15 changed files with 804 additions and 300 deletions
|
|
@ -51,10 +51,13 @@ class M365_Login_Auth {
|
|||
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' ) );
|
||||
// Button-only mode (works on wp-login.php and on custom login pages).
|
||||
add_action( 'init', array( $this, 'maybe_accept_fallback_key' ), 6 );
|
||||
// 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 );
|
||||
|
||||
// Custom login page: send people back there after logging out.
|
||||
add_filter( 'logout_redirect', array( $this, 'logout_redirect' ), 10, 3 );
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -84,7 +87,7 @@ class M365_Login_Auth {
|
|||
}
|
||||
|
||||
/**
|
||||
* wp-login.php?m365_fallback=KEY → sets the fallback cookie and reloads without the key in the URL.
|
||||
* ?m365_fallback=KEY (on any page) → sets the fallback cookie and reloads the login page without the key in the URL.
|
||||
*/
|
||||
public function maybe_accept_fallback_key() {
|
||||
if ( ! $this->settings->button_only() ) {
|
||||
|
|
@ -92,7 +95,7 @@ class M365_Login_Auth {
|
|||
}
|
||||
// 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 ) {
|
||||
if ( '' === $given || 'on' === $given ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +114,26 @@ class M365_Login_Auth {
|
|||
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() ) );
|
||||
wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', $this->settings->login_page_url() ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* After logout, return to the custom login page instead of wp-login.php.
|
||||
*
|
||||
* @param string $redirect_to Requested redirect.
|
||||
* @param string $requested_redirect_to Raw requested redirect.
|
||||
* @param WP_User|WP_Error $user User.
|
||||
* @return string
|
||||
*/
|
||||
public function logout_redirect( $redirect_to, $requested_redirect_to, $user ) {
|
||||
$custom = $this->settings->custom_login_url();
|
||||
if ( '' === $custom || '' !== (string) $requested_redirect_to ) {
|
||||
return $redirect_to;
|
||||
}
|
||||
return add_query_arg( 'loggedout', 'true', $custom );
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuses username/password sign-in on wp-login.php while button-only mode is active.
|
||||
*
|
||||
|
|
@ -130,13 +149,27 @@ class M365_Login_Auth {
|
|||
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'] ) {
|
||||
// Interactive password logins only: XML-RPC, REST (application passwords), WP-CLI and cron keep working.
|
||||
if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
|
||||
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
|
||||
|| ( defined( 'WP_CLI' ) && WP_CLI )
|
||||
|| wp_doing_cron() ) {
|
||||
return $user;
|
||||
}
|
||||
if ( ! $user instanceof WP_User ) {
|
||||
return $user; // Already failed for another reason; keep core's message.
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows exempting a password sign-in from button-only mode (e.g. a trusted membership plugin).
|
||||
*
|
||||
* @param bool $block Whether to block. Default true.
|
||||
* @param WP_User $user Authenticated user.
|
||||
*/
|
||||
if ( ! apply_filters( 'm365_login_block_password_login', true, $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) );
|
||||
}
|
||||
|
||||
|
|
@ -720,45 +753,48 @@ class M365_Login_Auth {
|
|||
*/
|
||||
private function fail( $code ) {
|
||||
$this->clear_state_cookie();
|
||||
wp_safe_redirect( add_query_arg( 'm365_error', rawurlencode( $code ), wp_login_url() ) );
|
||||
nocache_headers();
|
||||
wp_safe_redirect( add_query_arg( 'm365_error', rawurlencode( $code ), $this->settings->login_page_url() ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to the PHP error log when WP_DEBUG_LOG is enabled.
|
||||
* Messages for the current request (from ?m365_error and ?m365_fallback=on), for any login page.
|
||||
*
|
||||
* @param string $message Message.
|
||||
* @return array[] Each item: array( 'type' => 'error'|'message', 'code' => string, 'text' => string ).
|
||||
*/
|
||||
private function log( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
|
||||
error_log( '[M365 Login] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps error codes to messages on the login screen.
|
||||
*
|
||||
* @param WP_Error $errors Login errors.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function login_errors( $errors ) {
|
||||
if ( ! $errors instanceof WP_Error ) {
|
||||
$errors = new WP_Error();
|
||||
}
|
||||
|
||||
public function current_messages() {
|
||||
// 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
|
||||
|
||||
$out = array();
|
||||
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' );
|
||||
$out[] = array(
|
||||
'type' => 'message',
|
||||
'code' => 'fallback_on',
|
||||
'text' => __( 'Password sign-in is temporarily enabled for this browser (30 minutes).', 'm365-login' ),
|
||||
);
|
||||
}
|
||||
if ( '' === $code ) {
|
||||
return $errors;
|
||||
if ( '' !== $code ) {
|
||||
$messages = $this->error_messages();
|
||||
$out[] = array(
|
||||
'type' => 'access_denied' === $code ? 'message' : 'error',
|
||||
'code' => $code,
|
||||
'text' => isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
$messages = array(
|
||||
/**
|
||||
* Error code → translated message map.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function error_messages() {
|
||||
return array(
|
||||
'not_configured' => __( 'Microsoft login is not configured yet.', 'm365-login' ),
|
||||
'invalid_state' => __( 'The login request expired or was invalid. Please try again.', 'm365-login' ),
|
||||
'access_denied' => __( 'Microsoft sign-in was cancelled.', 'm365-login' ),
|
||||
|
|
@ -775,13 +811,21 @@ class M365_Login_Auth {
|
|||
'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
|
||||
'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
|
||||
);
|
||||
}
|
||||
|
||||
$errors->add(
|
||||
'm365_login_' . $code,
|
||||
isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],
|
||||
'access_denied' === $code ? 'message' : 'error'
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds the current messages to the wp-login.php error object.
|
||||
*
|
||||
* @param WP_Error $errors Login errors.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function login_errors( $errors ) {
|
||||
if ( ! $errors instanceof WP_Error ) {
|
||||
$errors = new WP_Error();
|
||||
}
|
||||
foreach ( $this->current_messages() as $msg ) {
|
||||
$errors->add( 'm365_login_' . $msg['code'], $msg['text'], $msg['type'] );
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue