Skip to content

Commit 5baa1f1

Browse files
committed
Improve error codes and middleware
1 parent b1efe1d commit 5baa1f1

File tree

11 files changed

+154
-128
lines changed

11 files changed

+154
-128
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ You can enable the following middleware using the "middlewares" config parameter
143143

144144
- "firewall": Limit access to specific IP addresses
145145
- "cors": Support for CORS requests (enabled by default)
146+
- "xsrf": Block XSRF attacks using the 'Double Submit Cookie' method
147+
- "ajaxOnly": Allow only AJAX requests to prevent XSRF attacks
146148
- "jwtAuth": Support for "Basic Authentication"
147149
- "basicAuth": Support for "Basic Authentication"
148150
- "authorization": Restrict access to certain tables or columns
@@ -160,7 +162,14 @@ You can tune the middleware behavior using middleware specific configuration par
160162
- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN")
161163
- "cors.allowMethods": The methods allowed in the CORS request ("OPTIONS, GET, PUT, POST, DELETE, PATCH")
162164
- "cors.allowCredentials": To allow credentials in the CORS request ("true")
165+
- "cors.exposeHeaders": Whitelist headers that browsers are allowed to access ("")
163166
- "cors.maxAge": The time that the CORS grant is valid in seconds ("1728000")
167+
- "xsrf.excludeMethods": The methods that do not require XSRF protection ("OPTIONS,GET")
168+
- "xsrf.cookieName": The name of the XSRF protection cookie ("XSRF-TOKEN")
169+
- "xsrf.headerName": The name of the XSRF protection header ("X-XSRF-TOKEN")
170+
- "ajaxOnly.excludeMethods": The methods that do not require AJAX ("OPTIONS,GET")
171+
- "ajaxOnly.headerName": The name of the required header ("X-Requested-With")
172+
- "ajaxOnly.headerValue": The value of the required header ("XMLHttpRequest")
164173
- "jwtAuth.mode": Set to "optional" if you want to allow anonymous access ("required")
165174
- "jwtAuth.header": Name of the header containing the JWT token ("X-Authorization")
166175
- "jwtAuth.leeway": The acceptable number of seconds of clock skew ("5")
@@ -808,6 +817,8 @@ The following errors may be reported:
808817
- 1014: Operation forbidden (403 FORBIDDEN)
809818
- 1015: Operation not supported (405 METHOD NOT ALLOWED)
810819
- 1016: Temporary or permanently blocked (403 FORBIDDEN)
820+
- 1017: Bad or missing XSRF token (403 FORBIDDEN)
821+
- 1018: Only AJAX requests allowed (403 FORBIDDEN)
811822
- 9999: Unknown error (500: INTERNAL SERVER ERROR)
812823

813824
The following JSON structure is used:

api.php

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2899,59 +2899,6 @@ public function handle(Request $request): Response
28992899

29002900
}
29012901

2902-
// file: src/Tqdev/PhpCrudApi/Middleware/Auth0Middleware.php
2903-
2904-
class Auth0Middleware extends Middleware
2905-
{
2906-
2907-
private function getFullUrl(String $path)
2908-
{
2909-
list($scheme, $default) = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? array('https', 443) : array('http', 80);
2910-
$port = ($_SERVER['SERVER_PORT'] == $default) ? '' : (':' . $_SERVER['SERVER_PORT']);
2911-
return $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . $path;
2912-
}
2913-
2914-
private function login(Request $request): Response
2915-
{
2916-
$domain = $this->getProperty('domain', '');
2917-
$clientId = $this->getProperty('clientId', '');
2918-
$redirectUri = $this->getFullUrl('/callback');
2919-
$url = "https://$domain/authorize?response_type=token&client_id=$clientId&redirect_uri=$redirectUri";
2920-
return $this->responder->redirect($url);
2921-
}
2922-
2923-
private function callback(Request $request): Response
2924-
{
2925-
$response = $this->responder->success('<h1>test</h1>');
2926-
$response->addHeader('Content-Type', 'text/html');
2927-
return $response;
2928-
}
2929-
2930-
private function logout(Request $request): Response
2931-
{
2932-
session_destroy();
2933-
$url = $this->getFullUrl('/login');
2934-
return $this->responder->redirect($url);
2935-
}
2936-
2937-
public function handle(Request $request): Response
2938-
{
2939-
if (session_status() == PHP_SESSION_NONE) {
2940-
session_start();
2941-
}
2942-
$path = $request->getPathSegment(1);
2943-
switch ($path) {
2944-
case 'login':
2945-
return $this->login($request);
2946-
case 'callback':
2947-
return $this->callback($request);
2948-
case 'logout':
2949-
return $this->logout($request);
2950-
}
2951-
return $this->next->handle($request);
2952-
}
2953-
}
2954-
29552902
// file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
29562903

29572904
class AuthorizationMiddleware extends Middleware
@@ -3116,6 +3063,9 @@ public function handle(Request $request): Response
31163063
if (!$validUser) {
31173064
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
31183065
}
3066+
if (!headers_sent()) {
3067+
session_regenerate_id();
3068+
}
31193069
}
31203070
if (!isset($_SESSION['username']) || !$_SESSION['username']) {
31213071
$authenticationMode = $this->getProperty('mode', 'required');
@@ -3158,19 +3108,33 @@ public function handle(Request $request): Response
31583108
} elseif ($method == 'OPTIONS') {
31593109
$response = new Response(Response::OK, '');
31603110
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN');
3161-
$response->addHeader('Access-Control-Allow-Headers', $allowHeaders);
3111+
if ($allowHeaders) {
3112+
$response->addHeader('Access-Control-Allow-Headers', $allowHeaders);
3113+
}
31623114
$allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
3163-
$response->addHeader('Access-Control-Allow-Methods', $allowMethods);
3115+
if ($allowMethods) {
3116+
$response->addHeader('Access-Control-Allow-Methods', $allowMethods);
3117+
}
31643118
$allowCredentials = $this->getProperty('allowCredentials', 'true');
3165-
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
3119+
if ($allowCredentials) {
3120+
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
3121+
}
31663122
$maxAge = $this->getProperty('maxAge', '1728000');
3167-
$response->addHeader('Access-Control-Max-Age', $maxAge);
3123+
if ($maxAge) {
3124+
$response->addHeader('Access-Control-Max-Age', $maxAge);
3125+
}
3126+
$exposeHeaders = $this->getProperty('exposeHeaders', '');
3127+
if ($exposeHeaders) {
3128+
$response->addHeader('Access-Control-Expose-Headers', $exposeHeaders);
3129+
}
31683130
} else {
31693131
$response = $this->next->handle($request);
31703132
}
31713133
if ($origin) {
31723134
$allowCredentials = $this->getProperty('allowCredentials', 'true');
3173-
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
3135+
if ($allowCredentials) {
3136+
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
3137+
}
31743138
$response->addHeader('Access-Control-Allow-Origin', $origin);
31753139
}
31763140
return $response;
@@ -3359,6 +3323,9 @@ public function handle(Request $request): Response
33593323
if (empty($claims)) {
33603324
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
33613325
}
3326+
if (!headers_sent()) {
3327+
session_regenerate_id();
3328+
}
33623329
}
33633330
if (empty($_SESSION['claims'])) {
33643331
$authenticationMode = $this->getProperty('mode', 'required');
@@ -3573,6 +3540,39 @@ public function handle(Request $request): Response
35733540
}
35743541
}
35753542

3543+
// file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
3544+
3545+
class XsrfMiddleware extends Middleware
3546+
{
3547+
private function getToken(): String
3548+
{
3549+
$cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
3550+
if (isset($_COOKIE[$cookieName])) {
3551+
$token = $_COOKIE[$cookieName];
3552+
} else {
3553+
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
3554+
$token = bin2hex(random_bytes(8));
3555+
if (!headers_sent()) {
3556+
setcookie($cookieName, $token, 0, '', '', $secure);
3557+
}
3558+
}
3559+
return $token;
3560+
}
3561+
3562+
public function handle(Request $request): Response
3563+
{
3564+
$token = $this->getToken();
3565+
$method = $request->getMethod();
3566+
if (!in_array($method, ['OPTIONS', 'GET'])) {
3567+
$headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
3568+
if ($token != $request->getHeader($headerName)) {
3569+
return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
3570+
}
3571+
}
3572+
return $this->next->handle($request);
3573+
}
3574+
}
3575+
35763576
// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
35773577

35783578
class OpenApiBuilder
@@ -4236,6 +4236,7 @@ class ErrorCode
42364236
const OPERATION_FORBIDDEN = 1014;
42374237
const OPERATION_NOT_SUPPORTED = 1015;
42384238
const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
4239+
const BAD_OR_MISSING_XSRF_TOKEN = 1017;
42394240

42404241
private $values = [
42414242
9999 => ["%s", Response::INTERNAL_SERVER_ERROR],
@@ -4256,6 +4257,7 @@ class ErrorCode
42564257
1014 => ["Operation forbidden", Response::FORBIDDEN],
42574258
1015 => ["Operation '%s' not supported", Response::METHOD_NOT_ALLOWED],
42584259
1016 => ["Temporary or permanently blocked", Response::FORBIDDEN],
4260+
1017 => ["Bad or missing XSRF token", Response::FORBIDDEN],
42594261
];
42604262

42614263
public function __construct(int $code)
@@ -5029,8 +5031,8 @@ public function __construct(Config $config)
50295031
case 'authorization':
50305032
new AuthorizationMiddleware($router, $responder, $properties, $reflection);
50315033
break;
5032-
case 'auth0':
5033-
new Auth0Middleware($router, $responder, $properties, $reflection);
5034+
case 'xsrf':
5035+
new XsrfMiddleware($router, $responder, $properties);
50345036
break;
50355037
case 'customization':
50365038
new CustomizationMiddleware($router, $responder, $properties, $reflection);
@@ -5395,7 +5397,7 @@ public function getHeader(String $key): String
53955397
return $this->headers[$key];
53965398
}
53975399
if ($this->highPerformance) {
5398-
$serverKey = 'HTTP_' . strtoupper(str_replace('_', '-', $key));
5400+
$serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
53995401
if (isset($_SERVER[$serverKey])) {
54005402
return $_SERVER[$serverKey];
54015403
}
@@ -5522,6 +5524,7 @@ public function __toString(): String
55225524
'username' => 'php-crud-api',
55235525
'password' => 'php-crud-api',
55245526
'database' => 'php-crud-api',
5527+
'middlewares' => 'xsrf',
55255528
]);
55265529
$request = new Request();
55275530
$api = new Api($config);

src/Tqdev/PhpCrudApi/Api.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Tqdev\PhpCrudApi\Controller\RecordController;
1111
use Tqdev\PhpCrudApi\Controller\Responder;
1212
use Tqdev\PhpCrudApi\Database\GenericDB;
13-
use Tqdev\PhpCrudApi\Middleware\Auth0Middleware;
1413
use Tqdev\PhpCrudApi\Middleware\AuthorizationMiddleware;
1514
use Tqdev\PhpCrudApi\Middleware\BasicAuthMiddleware;
1615
use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
@@ -21,6 +20,7 @@
2120
use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
2221
use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
2322
use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
23+
use Tqdev\PhpCrudApi\Middleware\XsrfMiddleware;
2424
use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
2525
use Tqdev\PhpCrudApi\Record\ErrorCode;
2626
use Tqdev\PhpCrudApi\Record\RecordService;
@@ -71,8 +71,8 @@ public function __construct(Config $config)
7171
case 'authorization':
7272
new AuthorizationMiddleware($router, $responder, $properties, $reflection);
7373
break;
74-
case 'auth0':
75-
new Auth0Middleware($router, $responder, $properties, $reflection);
74+
case 'xsrf':
75+
new XsrfMiddleware($router, $responder, $properties);
7676
break;
7777
case 'customization':
7878
new CustomizationMiddleware($router, $responder, $properties, $reflection);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
namespace Tqdev\PhpCrudApi\Middleware;
3+
4+
use Tqdev\PhpCrudApi\Controller\Responder;
5+
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
6+
use Tqdev\PhpCrudApi\Record\ErrorCode;
7+
use Tqdev\PhpCrudApi\Request;
8+
use Tqdev\PhpCrudApi\Response;
9+
10+
class AjaxOnlyMiddleware extends Middleware
11+
{
12+
public function handle(Request $request): Response
13+
{
14+
$method = $request->getMethod();
15+
$excludeMethods = $this->getProperty('excludeMethods', 'OPTIONS,GET');
16+
if (!in_array($method, $excludeMethods)) {
17+
$headerName = $this->getProperty('headerName', 'X-Requested-With');
18+
$headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
19+
if ($headerValue != $request->getHeader($headerName)) {
20+
return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, '');
21+
}
22+
}
23+
return $this->next->handle($request);
24+
}
25+
}

src/Tqdev/PhpCrudApi/Middleware/Auth0Middleware.php

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public function setNext(Handler $handler) /*: void*/
2222
$this->next = $handler;
2323
}
2424

25+
private function getArrayProperty(String $property, $default)
26+
{
27+
return isset($this->properties[$key]) ? array_filter(array_map('trim', explode(',', $this->properties[$key]))) : $default;
28+
}
29+
2530
protected function getProperty(String $key, $default)
2631
{
2732
return isset($this->properties[$key]) ? $this->properties[$key] : $default;

src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ private function getVerifiedClaims(String $token, int $time, int $leeway, int $t
6060
return $claims;
6161
}
6262

63-
private function getArrayProperty(String $property, String $default): array
64-
{
65-
return array_filter(array_map('trim', explode(',', $this->getProperty($property, $default))));
66-
}
67-
6863
private function getClaims(String $token): array
6964
{
7065
$time = (int) $this->getProperty('time', time());

0 commit comments

Comments
 (0)