Skip to content

Commit 78fac21

Browse files
committed
Added code for basic authentication plugin functionality
1 parent cc76438 commit 78fac21

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

pressable-basic-authentication.php

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

0 commit comments

Comments
 (0)