Commit c12f0683 authored by Emiliano Balbuena's avatar Emiliano Balbuena

(feat): XSRF token sync between domains using JWT

1 merge request!308WIP: (feat): Minds Pro
Pipeline #81913441 failed with stages
in 5 minutes and 1 second
......@@ -3,7 +3,7 @@
namespace Minds\Api;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain;
use Minds\Core\Pro\Domain\Security as ProDomainSecurity;
use Minds\Interfaces;
use Minds\Helpers;
use Minds\Core\Security;
......@@ -34,6 +34,7 @@ class Factory
$loop = count($segments);
while ($loop >= 0) {
$offset = $loop -1;
if ($loop < count($segments)) {
$slug_length = strlen($segments[$offset+1].'\\');
$route_length = strlen($route);
......@@ -44,32 +45,51 @@ class Factory
$actual = str_replace('\\', '/', $route);
if (isset(Routes::$routes[$actual])) {
$class_name = Routes::$routes[$actual];
if (class_exists($class_name)) {
$handler = new $class_name();
if (property_exists($handler, 'request')) {
$handler->request = $request;
}
if ($handler instanceof Interfaces\ApiAdminPam) {
self::adminCheck();
}
if (!$handler instanceof Interfaces\ApiIgnorePam) {
self::pamCheck($request, $response);
}
$pages = array_splice($segments, $loop) ?: [];
return $handler->$method($pages);
}
}
//autloaded routes
$class_name = "\\Minds\\Controllers\api\\$route";
if (class_exists($class_name)) {
$handler = new $class_name();
if (property_exists($handler, 'request')) {
$handler->request = $request;
}
if ($handler instanceof Interfaces\ApiAdminPam) {
self::adminCheck();
}
if (!$handler instanceof Interfaces\ApiIgnorePam) {
self::pamCheck($request, $response);
}
$pages = array_splice($segments, $loop) ?: [];
return $handler->$method($pages);
}
--$loop;
}
}
......@@ -80,13 +100,9 @@ class Factory
*/
public static function pamCheck($request, $response)
{
/** @var Domain $proDomain */
$proDomain = Di::_()->get('Pro\Domain');
if (
$request->getAttribute('oauth_user_id') ||
Security\XSRF::validateRequest() ||
$proDomain->validateRequest($request)
Security\XSRF::validateRequest()
) {
return true;
} else {
......
<?php
/**
* Jwt
* @author edgebal
*/
namespace Minds\Common;
use Exception;
use Firebase\JWT\JWT as FirebaseJWT;
class Jwt
{
/** @var string */
protected $key;
/**
* @param string $key
* @return Jwt
*/
public function setKey(string $key)
{
$this->key = $key;
return $this;
}
/**
* @param object|array $payload
* @return string
* @throws Exception
*/
public function encode($payload)
{
if (!$this->key) {
throw new Exception('Invalid JWT key');
}
return FirebaseJWT::encode($payload, $this->key, 'HS256');
}
/**
* @param string $jwt
* @return object
* @throws Exception
*/
public function decode($jwt)
{
if (!$this->key) {
throw new Exception('Invalid JWT key');
}
return FirebaseJWT::decode($jwt, $this->key, ['HS256']);
}
/**
* @return string
*/
public function randomString()
{
$bytes = openssl_random_pseudo_bytes(128);
return hash('sha512', $bytes);
}
}
......@@ -8,7 +8,6 @@
namespace Minds\Controllers\api\v1;
use Minds\Core;
use Minds\Core\Pro\Domain;
use Minds\Core\Security;
use Minds\Core\Session;
use Minds\Core\Features;
......@@ -20,6 +19,8 @@ use Minds\Exceptions\TwoFactorRequired;
class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
{
public $request;
/**
* NOT AVAILABLE
*/
......@@ -40,16 +41,7 @@ class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
*/
public function post($pages)
{
/** @var Domain $proDomain */
$proDomain = Di::_()->get('Pro\Domain');
// TODO: Implement server request
$serverRequest = new \Zend\Diactoros\ServerRequest();
if (
!Core\Security\XSRF::validateRequest() &&
!$proDomain->validateRequest($serverRequest)
) {
if (!Core\Security\XSRF::validateRequest()) {
return false;
}
......
......@@ -69,10 +69,6 @@ class settings implements Interfaces\Api
{
Factory::isLoggedIn();
if (!Core\Security\XSRF::validateRequest()) {
//return false;
}
if (Core\Session::getLoggedInUser()->isAdmin() && isset($pages[0])) {
$user = new entities\User($pages[0]);
} else {
......
......@@ -13,6 +13,8 @@ use Minds\Interfaces;
class facebook implements Interfaces\Api, Interfaces\ApiIgnorePam
{
public $request;
/**
* Get request
* @param array $pages
......
......@@ -17,6 +17,8 @@ use Minds\Interfaces;
class twofactor implements Interfaces\Api
{
public $request;
/**
* NOT AVAILABLE
*/
......
......@@ -10,7 +10,6 @@ use Exception;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Entities\User;
use Zend\Diactoros\ServerRequest;
class Domain
{
......@@ -52,11 +51,6 @@ class Domain
return $settings;
}
public function validateRequest(ServerRequest $request)
{
return true;
}
/**
* @param Settings $settings
* @return string
......
<?php
/**
* Security
* @author edgebal
*/
namespace Minds\Core\Pro\Domain;
use Exception;
use Minds\Common\Cookie;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Zend\Diactoros\ServerRequest;
class Security
{
/** @var string */
const JWT_COOKIE_NAME = 'PRO-XSRF-JWT';
/** @var string */
const XSRF_COOKIE_NAME = 'XSRF-TOKEN';
/** @var Cookie */
protected $cookie;
/** @var Jwt */
protected $jwt;
/** @var Config */
protected $config;
/**
* Security constructor.
* @param Cookie $cookie
* @param Jwt $jwt
* @param Config $config
*/
public function __construct(
$cookie = null,
$jwt = null,
$config = null
)
{
$this->cookie = $cookie ?: new Cookie();
$this->jwt = $jwt ?: new Jwt();
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param string $domain
* @return string
* @throws Exception
*/
public function setUp($domain)
{
$nonce = $this->jwt->randomString();
$now = time();
$exp = $now + 60;
$jwt = $this->jwt
->setKey($this->getEncryptionKey())
->encode([
'iss' => 'minds.com',
'aud' => strtolower($domain),
'nbf' => $now,
'exp' => $exp,
'nonce' => $nonce,
]);
$this->cookie
->setName(static::JWT_COOKIE_NAME)
->setValue($jwt)
->setExpire($exp)
->setPath('/')
->setHttpOnly(false)
->create();
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($nonce)
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
return $jwt;
}
/**
* @param ServerRequest $request
*/
public function syncCookies(ServerRequest $request)
{
$jwt = $request->getServerParams()['HTTP_X_PRO_XSRF_JWT'] ?? '';
if (!$jwt) {
return;
}
try {
$data = $this->jwt
->setKey($this->getEncryptionKey())
->decode($jwt);
if (($_COOKIE[static::XSRF_COOKIE_NAME] ?? null) === $data->nonce) {
return;
}
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($data->nonce)
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
} catch (Exception $e) { }
}
/**
* @return string
*/
protected function getEncryptionKey()
{
return $this->config->get('oauth')['encryption_key'] ?? '';
}
}
......@@ -24,6 +24,10 @@ class ProProvider extends Provider
return new Domain();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain\Security', function ($di) {
return new Domain\Security();
}, ['useFactory' => true]);
$this->di->bind('Pro\SEO', function ($di) {
return new SEO();
}, ['useFactory' => true]);
......
......@@ -6,6 +6,7 @@
namespace Minds\Core\Router\Middleware;
use Exception;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Pro\Manager;
use Minds\Core\Di\Di;
......@@ -19,6 +20,9 @@ class ProMiddleware implements RouterMiddleware
/** @var Domain */
protected $domain;
/** @var Domain\Security */
protected $domainSecurity;
/** @var Manager */
protected $manager;
......@@ -31,17 +35,20 @@ class ProMiddleware implements RouterMiddleware
/**
* ProMiddleware constructor.
* @param Domain $domain
* @param Domain\Security $domainSecurity
* @param Manager $manager
* @param SEO $seo
* @param EntitiesBuilder $entitiesBuilder
*/
public function __construct(
$domain = null,
$domainSecurity = null,
$manager = null,
$seo = null,
$entitiesBuilder = null
) {
$this->domain = $domain ?: Di::_()->get('Pro\Domain');
$this->domainSecurity = $domainSecurity ?: Di::_()->get('Pro\Domain\Security');
$this->manager = $manager ?: Di::_()->get('Pro\Manager');
$this->seo = $seo ?: Di::_()->get('Pro\SEO');
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
......@@ -51,13 +58,15 @@ class ProMiddleware implements RouterMiddleware
* @param ServerRequest $request
* @param JsonResponse $response
* @return false|null|void
* @throws Exception
*/
public function onRequest(ServerRequest $request, JsonResponse &$response)
{
$serverParams = $request->getServerParams() ?? [];
$originalHost = $serverParams['HTTP_HOST'];
$scheme = $request->getUri()->getScheme();
$host = parse_url($serverParams['HTTP_ORIGIN'] ?? '', PHP_URL_HOST) ?: $serverParams['HTTP_HOST'];
$host = parse_url($serverParams['HTTP_ORIGIN'] ?? '', PHP_URL_HOST) ?: $originalHost;
if (!$host) {
return;
......@@ -73,7 +82,7 @@ class ProMiddleware implements RouterMiddleware
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,X-No-Cache,x-xsrf-token,x-minds-origin,x-version');
header('Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,X-No-Cache,x-xsrf-token,x-pro-xsrf-jwt,x-minds-origin,x-version');
if ($request->getMethod() === 'OPTIONS') {
return false;
......@@ -94,5 +103,16 @@ class ProMiddleware implements RouterMiddleware
$this->seo
->setUser($user)
->setup($settings);
// Initialize XRSF JWT cookie, only if we're on Pro domain's scope
// If not and within 1 minute, update XSRF cookie to match it
if ($originalHost === $settings->getDomain()) {
$this->domainSecurity
->setUp($settings->getDomain());
} else {
$this->domainSecurity
->syncCookies($request);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment