Current File : /home/kelaby89/cartel.express/wp-content/plugins/ai-engine/labs/oauth.php |
<?php
// NOTE: This OAuth implementation is currently disabled in MCP due to
// security issues (unvalidated redirect URIs). The class remains for
// reference but is not loaded anywhere. Do not rely on this code until
// proper client registration and redirect URI validation is implemented.
class Meow_MWAI_Labs_OAuth {
private $core = null;
private $namespace = 'mcp/oauth';
private $codes_option = 'mwai_oauth_codes';
private $tokens_option = 'mwai_oauth_tokens';
private $code_lifetime = 600; // 10 minutes
private $token_lifetime = 3600; // 1 hour
private $logging = false;
public function __construct( $core, $logging = false ) {
$this->core = $core;
$this->logging = $logging;
add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
add_action( 'init', [ $this, 'handle_well_known' ] );
// Cleanup expired authorization codes and access tokens on a schedule.
// When OAuth is enabled, this ensures tokens don't accumulate forever.
add_action( 'mwai_cleanup_oauth', [ $this, 'cleanup_expired' ] );
if ( !wp_next_scheduled( 'mwai_cleanup_oauth' ) ) {
wp_schedule_event( time(), 'hourly', 'mwai_cleanup_oauth' );
}
}
public function rest_api_init() {
// Authorization endpoint
register_rest_route( $this->namespace, '/authorize', [
'methods' => 'GET',
'callback' => [ $this, 'handle_authorize' ],
'permission_callback' => '__return_true',
] );
// Token endpoint
register_rest_route( $this->namespace, '/token', [
'methods' => 'POST',
'callback' => [ $this, 'handle_token' ],
'permission_callback' => '__return_true',
] );
}
// Handle .well-known/oauth-authorization-server
public function handle_well_known() {
if ( $_SERVER['REQUEST_URI'] === '/.well-known/oauth-authorization-server' ) {
if ( $this->logging ) {
error_log( '[OAuth] 🌐 Discovery endpoint requested.' );
}
header( 'Content-Type: application/json' );
$base_url = get_site_url();
$discovery = [
'issuer' => $base_url,
'authorization_endpoint' => $base_url . '/wp-json/mcp/oauth/authorize',
'token_endpoint' => $base_url . '/wp-json/mcp/oauth/token',
'scopes_supported' => [ 'mcp' ],
'response_types_supported' => [ 'code' ],
'grant_types_supported' => [ 'authorization_code' ],
'token_endpoint_auth_methods_supported' => [ 'none' ],
'code_challenge_methods_supported' => [ 'S256' ]
];
echo json_encode( $discovery, JSON_PRETTY_PRINT );
exit;
}
}
// Authorization endpoint
public function handle_authorize( $request ) {
if ( $this->logging ) {
error_log( '[OAuth] 🔐 Authorize: ' . $request->get_param( 'client_id' ) );
}
$response_type = $request->get_param( 'response_type' );
$client_id = $request->get_param( 'client_id' );
$redirect_uri = $request->get_param( 'redirect_uri' );
$state = $request->get_param( 'state' );
$code_challenge = $request->get_param( 'code_challenge' );
$code_challenge_method = $request->get_param( 'code_challenge_method' );
$scope = $request->get_param( 'scope' );
// Validate request
if ( $response_type !== 'code' ) {
return new WP_Error( 'invalid_request', 'Invalid response_type' );
}
if ( empty( $client_id ) || empty( $redirect_uri ) || empty( $code_challenge ) ) {
return new WP_Error( 'invalid_request', 'Missing required parameters' );
}
if ( $code_challenge_method && $code_challenge_method !== 'S256' ) {
return new WP_Error( 'invalid_request', 'Only S256 code challenge method is supported' );
}
// Check if user is logged in
if ( !is_user_logged_in() ) {
// Store OAuth params in session/transient
$session_key = 'oauth_' . wp_generate_password( 16, false );
set_transient( $session_key, [
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'state' => $state,
'code_challenge' => $code_challenge,
'scope' => $scope ?: 'mcp'
], 600 );
// Show login form
$this->show_login_form( $session_key );
exit;
}
// User is logged in, generate authorization code
$code = $this->generate_authorization_code(
get_current_user_id(),
$client_id,
$redirect_uri,
$code_challenge,
$scope ?: 'mcp'
);
// Redirect back with code
$redirect_params = [
'code' => $code,
'state' => $state
];
$redirect_url = add_query_arg( $redirect_params, $redirect_uri );
wp_redirect( $redirect_url );
exit;
}
// Token endpoint
public function handle_token( $request ) {
if ( $this->logging ) {
$params = $request->get_params();
// Don't log sensitive data like code_verifier
$safe_params = $params;
if ( isset( $safe_params['code_verifier'] ) ) {
$safe_params['code_verifier'] = '[REDACTED]';
}
error_log( '[OAuth] 🎫 Token exchange for client: ' . $request->get_param( 'client_id' ) );
}
$grant_type = $request->get_param( 'grant_type' );
$code = $request->get_param( 'code' );
$client_id = $request->get_param( 'client_id' );
$redirect_uri = $request->get_param( 'redirect_uri' );
$code_verifier = $request->get_param( 'code_verifier' );
// Validate grant type
if ( $grant_type !== 'authorization_code' ) {
return new WP_Error( 'unsupported_grant_type', 'Only authorization_code grant type is supported', [ 'status' => 400 ] );
}
// Validate required parameters
if ( empty( $code ) || empty( $client_id ) || empty( $redirect_uri ) || empty( $code_verifier ) ) {
return new WP_Error( 'invalid_request', 'Missing required parameters', [ 'status' => 400 ] );
}
// Validate authorization code
$codes = get_option( $this->codes_option, [] );
if ( !isset( $codes[ $code ] ) ) {
return new WP_Error( 'invalid_grant', 'Invalid authorization code', [ 'status' => 400 ] );
}
$code_data = $codes[ $code ];
// Check if code is expired
if ( time() > $code_data['expires'] ) {
unset( $codes[ $code ] );
update_option( $this->codes_option, $codes );
return new WP_Error( 'invalid_grant', 'Authorization code has expired', [ 'status' => 400 ] );
}
// Validate client_id and redirect_uri
if ( $code_data['client_id'] !== $client_id || $code_data['redirect_uri'] !== $redirect_uri ) {
return new WP_Error( 'invalid_grant', 'Invalid client_id or redirect_uri', [ 'status' => 400 ] );
}
// Verify PKCE
$verifier_hash = base64_encode( hash( 'sha256', $code_verifier, true ) );
$verifier_hash = rtrim( strtr( $verifier_hash, '+/', '-_' ), '=' ); // Base64 URL encoding
if ( $verifier_hash !== $code_data['code_challenge'] ) {
return new WP_Error( 'invalid_grant', 'Invalid code_verifier', [ 'status' => 400 ] );
}
// Code is valid, remove it (one-time use)
unset( $codes[ $code ] );
update_option( $this->codes_option, $codes );
// Generate access token
$access_token = $this->generate_access_token( $code_data['user_id'], $code_data['scope'] );
// Return token response
return [
'access_token' => $access_token,
'token_type' => 'Bearer',
'expires_in' => $this->token_lifetime,
'scope' => $code_data['scope']
];
}
// Generate authorization code
private function generate_authorization_code( $user_id, $client_id, $redirect_uri, $code_challenge, $scope ) {
if ( $this->logging ) {
error_log( '[OAuth] ✅ Auth code generated for user ' . $user_id . '.' );
}
$code = wp_generate_password( 32, false );
$codes = get_option( $this->codes_option, [] );
$codes[ $code ] = [
'user_id' => $user_id,
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'code_challenge' => $code_challenge,
'scope' => $scope,
'expires' => time() + $this->code_lifetime
];
update_option( $this->codes_option, $codes );
return $code;
}
// Generate access token
private function generate_access_token( $user_id, $scope ) {
if ( $this->logging ) {
error_log( '[OAuth] ✅ Access token generated for user ' . $user_id . '.' );
}
$token = wp_generate_password( 40, false );
$tokens = get_option( $this->tokens_option, [] );
$tokens[ $token ] = [
'user_id' => $user_id,
'scope' => $scope,
'expires' => time() + $this->token_lifetime
];
update_option( $this->tokens_option, $tokens );
return $token;
}
// Validate access token
public function validate_token( $token ) {
$tokens = get_option( $this->tokens_option, [] );
if ( !isset( $tokens[ $token ] ) ) {
return false;
}
$token_data = $tokens[ $token ];
// Check if expired
if ( time() > $token_data['expires'] ) {
if ( $this->logging ) {
error_log( '[OAuth] ❌ Token validation failed: expired.' );
}
unset( $tokens[ $token ] );
update_option( $this->tokens_option, $tokens );
return false;
}
if ( $this->logging ) {
error_log( '[OAuth] ✅ Token valid for user ' . $token_data['user_id'] . '.' );
}
return $token_data;
}
// Show login form
private function show_login_form( $session_key ) {
$login_url = wp_login_url( add_query_arg( 'oauth_session', $session_key, $_SERVER['REQUEST_URI'] ) );
?>
<!DOCTYPE html>
<html>
<head>
<title>Login Required</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #f5f5f5;
}
.login-container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
}
h2 {
margin-top: 0;
color: #333;
}
p {
color: #666;
margin-bottom: 30px;
}
.login-button {
display: inline-block;
background: #0073aa;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
font-weight: 500;
}
.login-button:hover {
background: #005a87;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Authorization Required</h2>
<p>Please log in to authorize access to your MCP connector.</p>
<a href="<?php echo esc_url( $login_url ); ?>" class="login-button">Log In</a>
</div>
</body>
</html>
<?php
}
// Handle OAuth callback after login
public function handle_oauth_callback() {
if ( isset( $_GET['oauth_session'] ) && is_user_logged_in() ) {
if ( $this->logging ) {
error_log( '[OAuth] 🔄 Callback handling for session.' );
}
$session_key = sanitize_text_field( $_GET['oauth_session'] );
$oauth_params = get_transient( $session_key );
if ( $oauth_params ) {
delete_transient( $session_key );
// Generate authorization code
$code = $this->generate_authorization_code(
get_current_user_id(),
$oauth_params['client_id'],
$oauth_params['redirect_uri'],
$oauth_params['code_challenge'],
$oauth_params['scope']
);
// Redirect back with code
$redirect_params = [
'code' => $code,
'state' => $oauth_params['state']
];
$redirect_url = add_query_arg( $redirect_params, $oauth_params['redirect_uri'] );
wp_redirect( $redirect_url );
exit;
}
}
}
// Clean up expired tokens and codes
public function cleanup_expired() {
if ( $this->logging ) {
error_log( '[OAuth] 🧹 Cleaning expired tokens.' );
}
$now = time();
// Clean codes
$codes = get_option( $this->codes_option, [] );
foreach ( $codes as $code => $data ) {
if ( $now > $data['expires'] ) {
unset( $codes[ $code ] );
}
}
update_option( $this->codes_option, $codes );
// Clean tokens
$tokens = get_option( $this->tokens_option, [] );
foreach ( $tokens as $token => $data ) {
if ( $now > $data['expires'] ) {
unset( $tokens[ $token ] );
}
}
update_option( $this->tokens_option, $tokens );
}
}