PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
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 trunk, at vendor/wp-media/mcp-oauth/inc/Auth/ConsentEndpoint.php

139 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 'iss' => home_url(),
93 ],
94 $redirect_uri
95 )
96 );
97 exit;
98 }
99
100 // User allowed — issue a single-use auth code.
101 $auth_code = bin2hex( random_bytes( 32 ) );
102
103 set_transient(
104 'mcp_oauth_code_' . $auth_code,
105 [
106 'user_id' => $user_id,
107 'client_id' => $state_data['client_id'] ?? '',
108 'client_name' => $state_data['client_name'] ?? '',
109 'code_challenge' => $state_data['code_challenge'] ?? '',
110 'redirect_uri' => $redirect_uri,
111 ],
112 self::CODE_TTL
113 );
114
115 McpLogger::log(
116 'CONSENT',
117 'user granted access, auth code issued',
118 [
119 'user_id' => $user_id,
120 'client_id' => $state_data['client_id'] ?? '',
121 'redirect_uri' => $redirect_uri,
122 'code_ttl_s' => self::CODE_TTL,
123 ]
124 );
125
126 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.
127 add_query_arg(
128 [
129 'code' => $auth_code,
130 'state' => $state,
131 'iss' => home_url(),
132 ],
133 $redirect_uri
134 )
135 );
136 exit;
137 }
138 }
139