Skip to content

Commit 2fbff1c

Browse files
committed
Merge branch 'develop'
2 parents afb66f6 + 481f3a4 commit 2fbff1c

31 files changed

Lines changed: 1811 additions & 49 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
APP_NAME=CodefyPHP
33
APP_ENV=development
44
APP_KEY=changeme
5+
APP_SALT=pleasechangeme
56
APP_DEBUG=false
67
APP_BASE_PATH=
78
APP_BASE_URL=http://localhost:8080
@@ -18,6 +19,7 @@ MAILER_FROM_NAME="${APP_NAME}"
1819
MAILER_SENDMAIL_PATH=
1920

2021
# Database setup.
22+
DB_CONNECTION=default
2123
DB_DRIVER=mysql
2224
DB_HOST=localhost
2325
DB_NAME=test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ App/Domain/Post
22
App/Domain/PostType
33
App/Domain/Site
44
App/Domain/User
5+
App/Infrastructure/Http/Middleware/UserAuthMiddleware.php
56
database/migrations/.migrations.log
67
resources/views/*.tpl
78
storage/logs/*.log

App/Infrastructure/Http/Controllers/HomeController.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,26 @@
55
namespace App\Infrastructure\Http\Controllers;
66

77
use Codefy\Framework\Http\BaseController;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Qubus\Http\Session\SessionService;
11+
use Qubus\Routing\Router;
812
use Qubus\View\Native\Exception\InvalidTemplateNameException;
913
use Qubus\View\Native\Exception\ViewException;
14+
use Qubus\View\Renderer;
1015

1116
final class HomeController extends BaseController
1217
{
18+
public function __construct(
19+
SessionService $sessionService,
20+
ServerRequestInterface $request,
21+
ResponseInterface $response,
22+
Router $router,
23+
?Renderer $view = null
24+
) {
25+
parent::__construct($sessionService, $request, $response, $router, $view);
26+
}
27+
1328
/**
1429
* @throws ViewException
1530
* @throws InvalidTemplateNameException
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Infrastructure\Http\Middleware;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
use Qubus\Config\ConfigContainer;
12+
use Qubus\Exception\Exception;
13+
14+
final class CorsMiddleware implements MiddlewareInterface
15+
{
16+
public function __construct(protected ConfigContainer $configContainer)
17+
{
18+
}
19+
20+
/**
21+
* @inheritDoc
22+
* @throws Exception
23+
*/
24+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
25+
{
26+
$response = $handler->handle($request);
27+
28+
$origin = $response->getHeaderLine('origin');
29+
30+
if($this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin')[0] !== '*') {
31+
if (!$origin || !in_array($origin, $this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin'))) {
32+
return $response;
33+
}
34+
}
35+
36+
$headers = $this->configContainer->getConfigKey(key: 'cors');
37+
38+
foreach($headers as $key => $value) {
39+
$response = $response->withAddedHeader($key, implode(separator: ',', array: $value));
40+
}
41+
42+
return $response;
43+
}
44+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Infrastructure\Http\Middleware\Csrf;
6+
7+
use App\Infrastructure\Http\Middleware\Csrf\Traits\CsrfTokenAware;
8+
use App\Shared\Http\RequestMethod;
9+
use Psr\Http\Message\ResponseInterface;
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Psr\Http\Server\MiddlewareInterface;
12+
use Psr\Http\Server\RequestHandlerInterface;
13+
use Qubus\Config\ConfigContainer;
14+
use Qubus\Exception\Exception;
15+
use Qubus\Http\Factories\JsonResponseFactory;
16+
use Qubus\Http\Session\SessionService;
17+
18+
final class CsrfProtectionMiddleware implements MiddlewareInterface
19+
{
20+
use CsrfTokenAware;
21+
22+
public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService)
23+
{
24+
}
25+
26+
/**
27+
* @inheritDoc
28+
* @throws Exception
29+
* @throws \Exception
30+
*/
31+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
32+
{
33+
$response = $handler->handle($request);
34+
35+
if(true === $this->needsProtection($request) && ! $this->tokensMatch($request)) {
36+
return JsonResponseFactory::create(
37+
data: 'Bad CSRF Token.',
38+
status: $this->configContainer->getConfigKey('csrf.error_status_code')
39+
);
40+
}
41+
42+
return $response;
43+
}
44+
45+
/**
46+
* Check for methods not defined as safe.
47+
*
48+
* @param ServerRequestInterface $request
49+
* @return bool
50+
*/
51+
private function needsProtection(ServerRequestInterface $request): bool
52+
{
53+
return RequestMethod::isSafe($request->getMethod()) === false;
54+
}
55+
56+
/**
57+
* @throws Exception
58+
*/
59+
private function tokensMatch(ServerRequestInterface $request): bool
60+
{
61+
$expected = $this->fetchToken($request);
62+
$provided = $this->getTokenFromRequest($request);
63+
64+
return hash_equals($expected, $provided);
65+
}
66+
67+
68+
/**
69+
* @throws Exception
70+
* @throws \Exception
71+
*/
72+
private function fetchToken(ServerRequestInterface $request): string
73+
{
74+
$token = $request->getAttribute(CsrfTokenMiddleware::SESSION_ATTRIBUTE);
75+
76+
// Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format.
77+
if (is_string($token) &&
78+
ctype_alnum($token) &&
79+
strlen($token) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length')
80+
) {
81+
return $token;
82+
}
83+
84+
return '';
85+
}
86+
87+
/**
88+
* @throws Exception
89+
*/
90+
private function getTokenFromRequest(ServerRequestInterface $request): string
91+
{
92+
if ($request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header'))) {
93+
return (string) $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header'));
94+
}
95+
96+
// Handle the case for a POST form.
97+
$body = $request->getParsedBody();
98+
99+
if (is_array(
100+
$body) &&
101+
isset($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')]) &&
102+
is_string($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')])) {
103+
return $body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')];
104+
}
105+
106+
return '';
107+
}
108+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Infrastructure\Http\Middleware\Csrf;
6+
7+
use Qubus\Http\Session\SessionEntity;
8+
9+
use function Qubus\Support\Helpers\is_null__;
10+
11+
final class CsrfSession implements SessionEntity
12+
{
13+
public ?string $csrfToken = null;
14+
15+
public function withCsrfToken(?string $csrfToken = null): self
16+
{
17+
$this->csrfToken = $csrfToken;
18+
19+
return $this;
20+
}
21+
22+
public function equals(string $token): bool
23+
{
24+
return !is_null__($this->csrfToken) && $this->csrfToken === $token;
25+
}
26+
27+
public function csrfToken(): string|null
28+
{
29+
return $this->csrfToken;
30+
}
31+
32+
public function clear(): void
33+
{
34+
if(!empty($this->csrfToken)) {
35+
unset($this->csrfToken);
36+
}
37+
}
38+
39+
public function isEmpty(): bool
40+
{
41+
return empty($this->csrfToken);
42+
}
43+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Infrastructure\Http\Middleware\Csrf;
6+
7+
use App\Infrastructure\Http\Middleware\Csrf\Traits\CsrfTokenAware;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\MiddlewareInterface;
11+
use Psr\Http\Server\RequestHandlerInterface;
12+
use Qubus\Config\ConfigContainer;
13+
use Qubus\Exception\Exception;
14+
use Qubus\Http\Session\SessionService;
15+
16+
final class CsrfTokenMiddleware implements MiddlewareInterface
17+
{
18+
use CsrfTokenAware;
19+
20+
public const SESSION_ATTRIBUTE = 'CSRF_TOKEN';
21+
22+
public static CsrfTokenMiddleware $current;
23+
24+
private string $token;
25+
26+
public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService)
27+
{
28+
self::$current = $this;
29+
}
30+
31+
/**
32+
* @throws Exception
33+
*/
34+
public static function getField(): string
35+
{
36+
return sprintf(
37+
'<input type="hidden" name="%s" value="%s">' . "\n",
38+
self::$current->getFieldAttr(),
39+
self::$current->token
40+
);
41+
}
42+
43+
/**
44+
* @throws Exception
45+
*/
46+
public function getFieldAttr(): string
47+
{
48+
return $this->configContainer->getConfigKey(key: 'csrf.csrf_token', default: '_token');
49+
}
50+
51+
/**
52+
* @inheritDoc
53+
* @throws Exception
54+
* @throws \Exception
55+
*/
56+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
57+
{
58+
SessionService::$options = [
59+
'cookie-name' => 'CSRFSESSID',
60+
'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400),
61+
];
62+
63+
$session = $this->sessionService->makeSession($request);
64+
65+
$this->token = $token = $this->prepareToken(session: $session);
66+
67+
/**
68+
* If true, the application will do a header check, if not,
69+
* it will expect data submitted via an HTML form tag.
70+
*/
71+
if($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) {
72+
$request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $token);
73+
}
74+
75+
$response = $handler->handle(
76+
$request
77+
->withAttribute(self::SESSION_ATTRIBUTE, $token)
78+
);
79+
80+
$csrf = $session->get(CsrfSession::class);
81+
$csrf->withCsrfToken($token);
82+
83+
return $this->sessionService->commitSession($response, $session);
84+
}
85+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Infrastructure\Http\Middleware\Csrf\Traits;
6+
7+
use Qubus\Exception\Exception;
8+
use Qubus\Http\Session\HttpSession;
9+
10+
trait CsrfTokenAware
11+
{
12+
/**
13+
* @throws Exception
14+
*/
15+
protected function generateToken(): string
16+
{
17+
$salt = $this->configContainer->getConfigKey(key: 'csrf.salt');
18+
19+
return sha1(string: uniqid(prefix: sha1(string: $salt), more_entropy: true));
20+
}
21+
22+
/**
23+
* @throws Exception
24+
*/
25+
protected function prepareToken(HttpSession $session): string
26+
{
27+
// Try to retrieve an existing token from the session.
28+
$token = $session->clientSessionId();
29+
30+
// If token isn't present in the session, we generate a new token.
31+
if ($token === '') {
32+
$token = $this->generateToken();
33+
}
34+
return hash_hmac(algo: 'sha256', data: $token, key: $this->configContainer->getConfigKey(key: 'csrf.salt'));
35+
}
36+
37+
/**
38+
* @throws Exception
39+
*/
40+
protected function hashEquals(string $knownString, string $userString): bool
41+
{
42+
return hash_equals(
43+
$knownString,
44+
hash_hmac(
45+
algo: 'sha256',
46+
data: $userString,
47+
key: $this->configContainer->getConfigKey(key: 'csrf.salt')
48+
)
49+
);
50+
}
51+
}

0 commit comments

Comments
 (0)