wpdb = $wpdb; $this->settings = $settings; } /** * Create OAuth2 database tables */ public function create_tables(): void { $charset_collate = $this->wpdb->get_charset_collate(); // OAuth2 Clients table $clients_table = $this->wpdb->prefix . 'oauth2_clients'; $sql_clients = "CREATE TABLE $clients_table ( client_id varchar(80) NOT NULL, client_secret varchar(255) NOT NULL, client_name varchar(255) NOT NULL, redirect_uri text NOT NULL, grant_types varchar(255) DEFAULT 'authorization_code', scope text, user_id bigint(20) NOT NULL, is_public tinyint(1) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (client_id), KEY user_id (user_id) ) $charset_collate;"; // Authorization Codes table $auth_codes_table = $this->wpdb->prefix . 'oauth2_authorization_codes'; $sql_auth_codes = "CREATE TABLE $auth_codes_table ( authorization_code varchar(255) NOT NULL, client_id varchar(80) NOT NULL, user_id bigint(20) NOT NULL, redirect_uri text, expires datetime NOT NULL, scope text, code_challenge varchar(255), code_challenge_method varchar(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (authorization_code), KEY client_id (client_id), KEY user_id (user_id), KEY expires (expires) ) $charset_collate;"; // Access Tokens table $access_tokens_table = $this->wpdb->prefix . 'oauth2_access_tokens'; $sql_access_tokens = "CREATE TABLE $access_tokens_table ( access_token varchar(255) NOT NULL, client_id varchar(80) NOT NULL, user_id bigint(20) NOT NULL, expires datetime NOT NULL, scope text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (access_token), KEY client_id (client_id), KEY user_id (user_id), KEY expires (expires) ) $charset_collate;"; // Refresh Tokens table $refresh_tokens_table = $this->wpdb->prefix . 'oauth2_refresh_tokens'; $sql_refresh_tokens = "CREATE TABLE $refresh_tokens_table ( refresh_token varchar(255) NOT NULL, client_id varchar(80) NOT NULL, user_id bigint(20) NOT NULL, expires datetime NOT NULL, scope text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (refresh_token), KEY client_id (client_id), KEY user_id (user_id), KEY expires (expires) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql_clients); dbDelta($sql_auth_codes); dbDelta($sql_access_tokens); dbDelta($sql_refresh_tokens); } /** * Handle authorization endpoint request */ public function handle_authorization_request(): void { // Validate request method if ($_SERVER['REQUEST_METHOD'] !== 'GET') { $this->send_error_response(405, 'invalid_request', 'Authorization endpoint requires GET method'); return; } // Extract and validate parameters $client_id = sanitize_text_field($_GET['client_id'] ?? ''); $redirect_uri = esc_url_raw($_GET['redirect_uri'] ?? ''); $response_type = sanitize_text_field($_GET['response_type'] ?? ''); $scope = sanitize_text_field($_GET['scope'] ?? ''); $state = sanitize_text_field($_GET['state'] ?? ''); $code_challenge = sanitize_text_field($_GET['code_challenge'] ?? ''); $code_challenge_method = sanitize_text_field($_GET['code_challenge_method'] ?? ''); // Validate required parameters if (empty($client_id) || empty($redirect_uri) || empty($response_type)) { $this->send_error_response(400, 'invalid_request', 'Missing required parameters'); return; } // Validate response type if ($response_type !== 'code') { $this->send_error_response(400, 'unsupported_response_type', 'Only authorization code flow is supported'); return; } // Validate client $client = $this->get_client($client_id); if (!$client) { $this->send_error_response(400, 'invalid_client', 'Invalid client_id'); return; } // Validate redirect URI if (!$this->validate_redirect_uri($client, $redirect_uri)) { $this->send_error_response(400, 'invalid_redirect_uri', 'Invalid redirect_uri'); return; } // Validate PKCE (required for public clients) if ($client['is_public'] && (empty($code_challenge) || $code_challenge_method !== 'S256')) { $this->send_authorization_error($redirect_uri, 'invalid_request', 'PKCE required for public clients', $state); return; } // Check if user is logged in if (!is_user_logged_in()) { // Redirect to login with return URL $login_url = wp_login_url($_SERVER['REQUEST_URI']); wp_redirect($login_url); exit; } // Show authorization consent form $this->show_authorization_form([ 'client' => $client, 'redirect_uri' => $redirect_uri, 'scope' => $scope, 'state' => $state, 'code_challenge' => $code_challenge, 'code_challenge_method' => $code_challenge_method, ]); } /** * Handle token endpoint request */ public function handle_token_request(): void { // Validate request method if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->send_error_response(405, 'invalid_request', 'Token endpoint requires POST method'); return; } // Get grant type $grant_type = sanitize_text_field($_POST['grant_type'] ?? ''); switch ($grant_type) { case 'authorization_code': $this->handle_authorization_code_grant(); break; case 'refresh_token': $this->handle_refresh_token_grant(); break; case 'client_credentials': $this->handle_client_credentials_grant(); break; default: $this->send_error_response(400, 'unsupported_grant_type', 'Unsupported grant type'); } } /** * Handle authorization code grant */ private function handle_authorization_code_grant(): void { // Extract parameters $code = sanitize_text_field($_POST['code'] ?? ''); $client_id = sanitize_text_field($_POST['client_id'] ?? ''); $redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? ''); $code_verifier = sanitize_text_field($_POST['code_verifier'] ?? ''); // Get client credentials from Authorization header or POST $client_credentials = $this->extract_client_credentials(); if ($client_credentials) { $client_id = $client_credentials['client_id']; $client_secret = $client_credentials['client_secret']; } else { $client_secret = sanitize_text_field($_POST['client_secret'] ?? ''); } // Validate required parameters if (empty($code) || empty($client_id)) { $this->send_error_response(400, 'invalid_request', 'Missing required parameters'); return; } // Get and validate authorization code $auth_code = $this->get_authorization_code($code); if (!$auth_code) { $this->send_error_response(400, 'invalid_grant', 'Invalid authorization code'); return; } // Check if code has expired if (strtotime($auth_code['expires']) < time()) { $this->delete_authorization_code($code); $this->send_error_response(400, 'invalid_grant', 'Authorization code expired'); return; } // Validate client if ($auth_code['client_id'] !== $client_id) { $this->send_error_response(400, 'invalid_client', 'Client mismatch'); return; } // Get client details $client = $this->get_client($client_id); if (!$client) { $this->send_error_response(400, 'invalid_client', 'Invalid client'); return; } // Validate client secret for confidential clients if (!$client['is_public']) { if (empty($client_secret) || !password_verify($client_secret, $client['client_secret'])) { $this->send_error_response(401, 'invalid_client', 'Invalid client credentials'); return; } } // Validate redirect URI if (!empty($redirect_uri) && $auth_code['redirect_uri'] !== $redirect_uri) { $this->send_error_response(400, 'invalid_grant', 'Redirect URI mismatch'); return; } // Validate PKCE for public clients or if code challenge was provided if (!empty($auth_code['code_challenge'])) { if (empty($code_verifier)) { $this->send_error_response(400, 'invalid_request', 'Code verifier required'); return; } if (!$this->verify_pkce($code_verifier, $auth_code['code_challenge'], $auth_code['code_challenge_method'])) { $this->send_error_response(400, 'invalid_grant', 'Invalid code verifier'); return; } } // Generate tokens $access_token = $this->generate_access_token($client_id, $auth_code['user_id'], $auth_code['scope']); $refresh_token = $this->generate_refresh_token($client_id, $auth_code['user_id'], $auth_code['scope']); // Delete authorization code (one-time use) $this->delete_authorization_code($code); // Send token response $response = [ 'access_token' => $access_token, 'token_type' => 'Bearer', 'expires_in' => $this->settings['access_token_lifetime'] ?? 3600, 'refresh_token' => $refresh_token, 'scope' => $auth_code['scope'], ]; $this->send_json_response($response); } /** * Generate authorization code */ public function generate_authorization_code(array $params): string { $code = bin2hex(random_bytes(32)); $expires = date('Y-m-d H:i:s', time() + 600); // 10 minutes $this->wpdb->insert( $this->wpdb->prefix . 'oauth2_authorization_codes', [ 'authorization_code' => $code, 'client_id' => $params['client_id'], 'user_id' => get_current_user_id(), 'redirect_uri' => $params['redirect_uri'], 'expires' => $expires, 'scope' => $params['scope'] ?? '', 'code_challenge' => $params['code_challenge'] ?? '', 'code_challenge_method' => $params['code_challenge_method'] ?? '', ] ); return $code; } /** * Get authorization code from database */ private function get_authorization_code(string $code): ?array { $result = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}oauth2_authorization_codes WHERE authorization_code = %s", $code ), ARRAY_A ); return $result ?: null; } /** * Delete authorization code */ private function delete_authorization_code(string $code): void { $this->wpdb->delete( $this->wpdb->prefix . 'oauth2_authorization_codes', ['authorization_code' => $code] ); } /** * Generate access token */ private function generate_access_token(string $client_id, int $user_id, string $scope): string { $token = bin2hex(random_bytes(32)); $expires = date('Y-m-d H:i:s', time() + ($this->settings['access_token_lifetime'] ?? 3600)); $this->wpdb->insert( $this->wpdb->prefix . 'oauth2_access_tokens', [ 'access_token' => $token, 'client_id' => $client_id, 'user_id' => $user_id, 'expires' => $expires, 'scope' => $scope, ] ); return $token; } /** * Generate refresh token */ private function generate_refresh_token(string $client_id, int $user_id, string $scope): string { $token = bin2hex(random_bytes(32)); $expires = date('Y-m-d H:i:s', time() + ($this->settings['refresh_token_lifetime'] ?? 86400 * 30)); // 30 days $this->wpdb->insert( $this->wpdb->prefix . 'oauth2_refresh_tokens', [ 'refresh_token' => $token, 'client_id' => $client_id, 'user_id' => $user_id, 'expires' => $expires, 'scope' => $scope, ] ); return $token; } /** * Validate access token */ public function validate_access_token(string $token): ?array { $result = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}oauth2_access_tokens WHERE access_token = %s AND expires > NOW()", $token ), ARRAY_A ); return $result ?: null; } /** * Get client by client_id */ private function get_client(string $client_id): ?array { // Query WordPress posts to find OAuth2 client $posts = get_posts([ 'post_type' => 'oauth2_client', 'meta_key' => 'client_id', 'meta_value' => $client_id, 'posts_per_page' => 1, 'post_status' => 'publish' ]); if (empty($posts)) { return null; } $post = $posts[0]; $client_data = [ 'client_id' => get_post_meta($post->ID, 'client_id', true), 'client_secret' => get_post_meta($post->ID, 'client_secret', true), 'redirect_uris' => get_post_meta($post->ID, 'redirect_uris', true), 'grant_types' => get_post_meta($post->ID, 'grant_types', true), 'scope' => get_post_meta($post->ID, 'scope', true), 'is_public' => (bool)get_post_meta($post->ID, 'is_public', true), 'name' => $post->post_title, 'client_name' => $post->post_title // Add client_name for display ]; return $client_data; } /** * Validate redirect URI */ private function validate_redirect_uri(array $client, string $redirect_uri): bool { $redirect_uris = $client['redirect_uris'] ?? ''; if (empty($redirect_uris)) { return false; } $allowed_uris = explode(',', $redirect_uris); return in_array($redirect_uri, array_map('trim', $allowed_uris)); } /** * Verify PKCE code verifier */ private function verify_pkce(string $code_verifier, string $code_challenge, string $method): bool { if ($method !== 'S256') { return false; } $computed_challenge = rtrim(strtr(base64_encode(hash('sha256', $code_verifier, true)), '+/', '-_'), '='); return hash_equals($code_challenge, $computed_challenge); } /** * Extract client credentials from Authorization header */ private function extract_client_credentials(): ?array { $auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (strpos($auth_header, 'Basic ') === 0) { $credentials = base64_decode(substr($auth_header, 6)); $parts = explode(':', $credentials, 2); if (count($parts) === 2) { return [ 'client_id' => $parts[0], 'client_secret' => $parts[1], ]; } } return null; } /** * Show authorization consent form */ private function show_authorization_form(array $params): void { // Set content type header('Content-Type: text/html; charset=utf-8'); // Simple authorization form ?> >