-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpressable-basic-authentication.php
More file actions
341 lines (288 loc) · 8.75 KB
/
pressable-basic-authentication.php
File metadata and controls
341 lines (288 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<?php
/**
* Hosting Basic Authentication
*
* @package HostingBasicAuthentication
*/
/*
Plugin Name: Hosting Basic Authentication
Description: Forces all users to authenticate using Basic Authentication before accessing any page.
Version: 1.0.2
License: GPL2
Text Domain: hosting-basic-authentication
*/
// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
exit; // Prevent direct access
}
/**
* Main plugin class
*/
class Pressable_Basic_Auth {
/**
* Constructor
*/
public function __construct() {
// Hook into WordPress before anything is outputted.
add_action( 'plugins_loaded', array( $this, 'init' ), 1 );
// Add filter for logout URL.
add_filter( 'logout_url', array( $this, 'modify_logout_url' ), 10, 2 );
// Hook into login page early
add_action( 'login_init', array( $this, 'maybe_redirect_from_login_page' ), 0 );
}
/**
* Initialize the plugin
*/
public function init() {
// Skip if we're doing AJAX.
if ( $this->is_ajax_request() ) {
return;
}
// Skip if we're doing CRON.
if ( $this->is_cron_request() ) {
return;
}
// Skip if we're in CLI mode.
if ( $this->is_cli_request() ) {
return;
}
// Skip requests to excluded endpoints
if ($this->should_skip_auth()) {
return;
}
// Handle logout request.
if ( isset( $_GET['basic-auth-logout'] ) ) {
$this->handle_basic_auth_logout();
}
// Redirect from wp-login.php when already authenticated via Basic Auth
$this->maybe_redirect_from_login_page();
// Force authentication.
$this->force_basic_authentication();
}
/**
* Force Basic Authentication
*/
private function force_basic_authentication() {
// Prevent caching of authentication requests.
$this->prevent_caching();
// Extract credentials from headers.
$this->extract_basic_auth_credentials();
// Allow Super Admins to bypass authentication.
if ( is_multisite() && is_super_admin() ) {
return;
}
// Check if the user is already logged in.
if ( is_user_logged_in() ) {
return;
}
// Check for Basic Authentication credentials.
$auth_user = isset( $_SERVER['PHP_AUTH_USER'] ) ? sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) ) : null;
$auth_pass = isset( $_SERVER['PHP_AUTH_PW'] ) ? $_SERVER['PHP_AUTH_PW'] : null;
if ( ! $auth_user || ! $auth_pass ) {
$this->log_failed_auth( 'Missing credentials' );
$this->send_auth_headers();
}
// Validate credentials against WordPress users table.
$user = wp_authenticate( $auth_user, $auth_pass );
if ( is_wp_error( $user ) ) {
$this->log_failed_auth( "Invalid credentials for user: $auth_user" );
$this->send_auth_headers();
}
// Log the user in programmatically.
wp_set_current_user( $user->ID );
wp_set_auth_cookie( $user->ID );
}
/**
* Logs failed authentication attempts to the error log.
*
* @param string $message The message to log.
*/
private function log_failed_auth( $message ) {
error_log(
sprintf(
'[%s] Basic Auth Failed: %s',
gmdate( 'Y-m-d H:i:s' ),
$message
)
);
}
/**
* Check if the current request should skip authentication
*
* @return bool
*/
private function should_skip_auth() {
// List of endpoints to exclude from Basic Auth
$excluded_endpoints = array(
'xmlrpc.php',
'wp-json/jetpack',
'wp-json/wp/v2',
'wp-json/wp/v3'
);
// Get current request details
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$script_name = $_SERVER['SCRIPT_NAME'] ?? '';
// Check if this is a direct xmlrpc.php request
if (basename($script_name) === 'xmlrpc.php') {
return true;
}
// Check all excluded endpoints
foreach ($excluded_endpoints as $endpoint) {
if (strpos($request_uri, $endpoint) !== false) {
return true;
}
}
// Check WordPress constants
if (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) {
return true;
}
if (defined('REST_REQUEST') && REST_REQUEST) {
return true;
}
return false;
}
/**
* Sends authentication headers.
*/
private function send_auth_headers() {
header( 'WWW-Authenticate: Basic realm="Restricted Area"' );
header( 'HTTP/1.1 401 Unauthorized' );
echo '<h1>' . esc_html__( 'Authentication Required', 'pressable-basic-auth' ) . '</h1>';
exit;
}
/**
* Use getallheaders() for Servers That Strip Authorization Headers
*/
private function extract_basic_auth_credentials() {
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
return;
}
// Attempt to fetch credentials from Authorization header.
$auth_header = $this->get_authorization_header();
if ( ! $auth_header ) {
return;
}
if ( 0 === stripos( $auth_header, 'basic ' ) ) {
$auth_encoded = substr( $auth_header, 6 );
$auth_decoded = base64_decode( $auth_encoded );
if ( $auth_decoded && strpos( $auth_decoded, ':' ) !== false ) {
list( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) = explode( ':', $auth_decoded, 2 );
}
}
}
/**
* Get the authorization header
*
* @return string|null The authorization header value or null
*/
private function get_authorization_header() {
if ( function_exists( 'getallheaders' ) ) {
$headers = getallheaders();
// Check for Authorization header (case-insensitive).
foreach ( $headers as $key => $value ) {
if ( strtolower( $key ) === 'authorization' ) {
return $value;
}
}
}
// Try common alternative locations.
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] );
} elseif ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
return wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] );
}
return null;
}
/**
* Handles Basic Auth logout by forcing a 401 response and then redirecting.
*/
private function handle_basic_auth_logout() {
wp_logout(); // Log out from WordPress.
// Clear Basic Auth credentials by forcing a 401.
header( 'WWW-Authenticate: Basic realm="Restricted Area"' );
header( 'HTTP/1.1 401 Unauthorized' );
// Output a JavaScript-based redirect after the 401 response.
echo '<script>
setTimeout(function() {
window.location.href = "' . esc_url( home_url() ) . '";
}, 1000);
</script>';
// End execution to prevent further processing.
exit;
}
/**
* Modifies the default WordPress logout URL to trigger Basic Auth logout.
*
* @param string $logout_url The WordPress logout URL.
* @param string $redirect The redirect URL after logout.
* @return string Modified logout URL
*/
public function modify_logout_url( $logout_url, $redirect ) {
return add_query_arg( 'basic-auth-logout', '1', $logout_url );
}
/**
* Redirects from wp-login.php to home page when user is already authenticated via Basic Auth
*/
public function maybe_redirect_from_login_page() {
global $pagenow;
// Check if we're on the login page and have Basic Auth credentials
if ( 'wp-login.php' === $pagenow &&
! empty( $_SERVER['PHP_AUTH_USER'] ) &&
! empty( $_SERVER['PHP_AUTH_PW'] ) &&
! isset( $_GET['action'] ) &&
! isset( $_GET['loggedout'] ) &&
! isset( $_POST['log'] ) ) {
// Get appropriate home URL for either multisite or regular WordPress
if ( is_multisite() ) {
$redirect_url = network_home_url();
// If we can determine the current blog, go to its home instead
if ( isset( $_SERVER['HTTP_HOST'] ) ) {
$blog_details = get_blog_details( array( 'domain' => $_SERVER['HTTP_HOST'] ) );
if ( $blog_details ) {
$redirect_url = get_home_url( $blog_details->blog_id );
}
}
} else {
$redirect_url = home_url();
}
// Safe redirect
wp_safe_redirect( $redirect_url );
exit;
}
}
/**
* Prevent caching of authentication requests
*/
private function prevent_caching() {
header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
header( 'Pragma: no-cache' );
header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
}
/**
* Check if the current request is an AJAX request
*
* @return bool
*/
private function is_ajax_request() {
return ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ||
( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && 'xmlhttprequest' === strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) );
}
/**
* Check if the current request is a cron request
*
* @return bool
*/
private function is_cron_request() {
return defined( 'DOING_CRON' ) && DOING_CRON;
}
/**
* Check if the current request is a CLI request
*
* @return bool
*/
private function is_cli_request() {
return ( 'cli' === php_sapi_name() || ( defined( 'WP_CLI' ) && WP_CLI ) );
}
}
// Initialize the plugin.
new Pressable_Basic_Auth();