PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.2
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wp-media / mcp-oauth / inc / Auth / ConsentEndpoint.php

ConsentEndpoint.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.2, at vendor/wp-media/mcp-oauth/inc/Auth/ConsentEndpoint.php

137 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Consent Endpoint.
4 *
5 * Handles POST /oauth/consent — the form submission from the consent screen
6 * rendered by AuthorizeCallback. Verifies the WordPress nonce, looks up and
7 * consumes the state transient, then either issues an auth code (Allow) or
8 * redirects back to the client with error=access_denied (Deny).
9 *
10 * redirect_uri is always read from the server-side state transient, never from
11 * $_POST, so it cannot be manipulated by the user or a third party.
12 */
13
14 declare(strict_types=1);
15
16 namespace WPMedia\MCP\OAuth\Auth;
17
18 use WPMedia\MCP\OAuth\Logging\McpLogger;
19
20 class ConsentEndpoint {
21 /**
22 * Auth-code transient TTL (seconds). Codes are single-use; the transient
23 * is deleted immediately on redemption at the token endpoint.
24 */
25 const CODE_TTL = 60;
26
27 /**
28 * Handle the consent form POST.
29 *
30 * @return void
31 */
32 public function handle_request(): void {
33 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
34
35 if ( 'POST' !== $request_method ) {
36 McpLogger::log( 'CONSENT', 'rejected: wrong method', [ 'method' => $request_method ] );
37 wp_die( esc_html__( 'Method not allowed.', 'mcp-oauth' ), esc_html__( 'OAuth Error', 'mcp-oauth' ), [ 'response' => 405 ] );
38 }
39
40 if ( ! is_user_logged_in() ) {
41 McpLogger::log( 'CONSENT', 'rejected: user not logged in' );
42 wp_die( esc_html__( 'You must be logged in to authorise an MCP session.', 'mcp-oauth' ), esc_html__( 'OAuth Error', 'mcp-oauth' ), [ 'response' => 401 ] );
43 }
44
45 $state = sanitize_text_field( wp_unslash( $_POST['state'] ?? '' ) );
46 $action = sanitize_text_field( wp_unslash( $_POST['mcp_action'] ?? '' ) );
47
48 if ( '' === $state ) {
49 McpLogger::log( 'CONSENT', 'rejected: missing state' );
50 wp_die( esc_html__( 'Missing state parameter.', 'mcp-oauth' ), esc_html__( 'OAuth Error', 'mcp-oauth' ), [ 'response' => 400 ] );
51 }
52
53 // Nonce verification (CSRF protection) — must happen before consuming the transient.
54 check_admin_referer( 'mcp_consent_' . $state, 'mcp_consent_nonce' );
55
56 $state_key = 'mcp_oauth_state_' . $state;
57 $state_data = get_transient( $state_key );
58
59 if ( false === $state_data || ! is_array( $state_data ) ) {
60 McpLogger::log( 'CONSENT', 'rejected: state transient not found or expired', [ 'state' => $state ] );
61 wp_die( esc_html__( 'Your session has expired. Please restart the authorization flow.', 'mcp-oauth' ), esc_html__( 'OAuth Error', 'mcp-oauth' ), [ 'response' => 400 ] );
62 }
63
64 // Atomically consume the state — one-time use only. delete_transient()
65 // returns true for a single caller when requests race, so a double
66 // submission cannot mint two auth codes from one consent.
67 if ( ! delete_transient( $state_key ) ) {
68 McpLogger::log( 'CONSENT', 'rejected: state already consumed (concurrent submission)', [ 'state' => $state ] );
69 wp_die( esc_html__( 'Your session has expired. Please restart the authorization flow.', 'mcp-oauth' ), esc_html__( 'OAuth Error', 'mcp-oauth' ), [ 'response' => 400 ] );
70 }
71
72 $redirect_uri = (string) ( $state_data['redirect_uri'] ?? '' );
73 $user_id = get_current_user_id();
74
75 if ( 'allow' !== $action ) {
76 McpLogger::log(
77 'CONSENT',
78 'user denied access',
79 [
80 'user_id' => $user_id,
81 'client_id' => $state_data['client_id'] ?? '',
82 'mcp_action' => $action,
83 'redirect_uri' => $redirect_uri,
84 ]
85 );
86
87 wp_redirect( // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- redirecting to the client's registered redirect_uri sourced from the server-side state transient, not user input.
88 add_query_arg(
89 [
90 'error' => 'access_denied',
91 'state' => $state,
92 ],
93 $redirect_uri
94 )
95 );
96 exit;
97 }
98
99 // User allowed — issue a single-use auth code.
100 $auth_code = bin2hex( random_bytes( 32 ) );
101
102 set_transient(
103 'mcp_oauth_code_' . $auth_code,
104 [
105 'user_id' => $user_id,
106 'client_id' => $state_data['client_id'] ?? '',
107 'client_name' => $state_data['client_name'] ?? '',
108 'code_challenge' => $state_data['code_challenge'] ?? '',
109 'redirect_uri' => $redirect_uri,
110 ],
111 self::CODE_TTL
112 );
113
114 McpLogger::log(
115 'CONSENT',
116 'user granted access, auth code issued',
117 [
118 'user_id' => $user_id,
119 'client_id' => $state_data['client_id'] ?? '',
120 'redirect_uri' => $redirect_uri,
121 'code_ttl_s' => self::CODE_TTL,
122 ]
123 );
124
125 wp_redirect( // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- redirecting to the client's registered redirect_uri sourced from the server-side state transient, not user input.
126 add_query_arg(
127 [
128 'code' => $auth_code,
129 'state' => $state,
130 ],
131 $redirect_uri
132 )
133 );
134 exit;
135 }
136 }
137