fix: CSRF flow ships the raw token, not the HMAC cookie
This commit is contained in:
+22
-1
@@ -44,9 +44,15 @@ if ($secret === false || $secret === '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Ensure the __csrf cookie is set. If absent, issue a fresh token.
|
// 2. Ensure the __csrf cookie is set. If absent, issue a fresh token.
|
||||||
|
// The double-submit pattern stores the raw token in a separate readable
|
||||||
|
// cookie so client code (meta tag, JS) can echo it back as the
|
||||||
|
// X-CSRF-Token header. The HMAC cookie is the verification oracle;
|
||||||
|
// the raw-token cookie rides parallel and is readable by document.cookie
|
||||||
|
// or the equivalent server-side read.
|
||||||
$cookies = $_COOKIE;
|
$cookies = $_COOKIE;
|
||||||
$existingCookie = (string) ($cookies['__csrf'] ?? '');
|
$existingCookie = (string) ($cookies['__csrf'] ?? '');
|
||||||
if ($existingCookie === '') {
|
$existingRawToken = (string) ($cookies['__csrf_token'] ?? '');
|
||||||
|
if ($existingCookie === '' || $existingRawToken === '') {
|
||||||
[$token, $cookie] = CsrfToken::issue($secret);
|
[$token, $cookie] = CsrfToken::issue($secret);
|
||||||
setcookie('__csrf', $cookie, [
|
setcookie('__csrf', $cookie, [
|
||||||
'expires' => time() + 86400,
|
'expires' => time() + 86400,
|
||||||
@@ -55,8 +61,23 @@ if ($existingCookie === '') {
|
|||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax',
|
'samesite' => 'Lax',
|
||||||
]);
|
]);
|
||||||
|
// The raw token rides in a readable cookie so server-side templates
|
||||||
|
// can echo it into the <meta name="csrf-token"> tag without keeping
|
||||||
|
// the raw token in `index.php` per-request state. HttpOnly off so the
|
||||||
|
// server can read it via the request cookie jar; security comes from
|
||||||
|
// the HMAC verification, not from hiding the raw token (which the
|
||||||
|
// browser would already see in the meta tag anyway).
|
||||||
|
setcookie('__csrf_token', $token, [
|
||||||
|
'expires' => time() + 86400,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
|
'httponly' => false,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
$existingCookie = $cookie;
|
$existingCookie = $cookie;
|
||||||
|
$existingRawToken = $token;
|
||||||
$cookies['__csrf'] = $cookie;
|
$cookies['__csrf'] = $cookie;
|
||||||
|
$cookies['__csrf_token'] = $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Compute the upload-endpoint user token (derived from the same secret).
|
// 3. Compute the upload-endpoint user token (derived from the same secret).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class GetBattlefieldEditor
|
|||||||
/** @param array<string, string> $params */
|
/** @param array<string, string> $params */
|
||||||
public function handle(Request $request, array $params): Response
|
public function handle(Request $request, array $params): Response
|
||||||
{
|
{
|
||||||
$csrf = $request->cookies['__csrf'] ?? '';
|
$csrf = $request->cookies['__csrf_token'] ?? '';
|
||||||
$scenarioId = $params['id'] ?? 'new';
|
$scenarioId = $params['id'] ?? 'new';
|
||||||
$width = 8;
|
$width = 8;
|
||||||
$height = 8;
|
$height = 8;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ final class GetHomePage
|
|||||||
/** @param array<string, string> $params */
|
/** @param array<string, string> $params */
|
||||||
public function handle(Request $request, array $params): Response
|
public function handle(Request $request, array $params): Response
|
||||||
{
|
{
|
||||||
$csrf = $request->cookies['__csrf'] ?? '';
|
$csrf = $request->cookies['__csrf_token'] ?? '';
|
||||||
$bundled = $this->loadBundledScenarios();
|
$bundled = $this->loadBundledScenarios();
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class GetMatchView
|
|||||||
/** @param array<string, string> $params */
|
/** @param array<string, string> $params */
|
||||||
public function handle(Request $request, array $params): Response
|
public function handle(Request $request, array $params): Response
|
||||||
{
|
{
|
||||||
$csrf = $request->cookies['__csrf'] ?? '';
|
$csrf = $request->cookies['__csrf_token'] ?? '';
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
require_once __DIR__ . '/../../Views/layout.php';
|
require_once __DIR__ . '/../../Views/layout.php';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class GetTeamEditor
|
|||||||
/** @param array<string, string> $params */
|
/** @param array<string, string> $params */
|
||||||
public function handle(Request $request, array $params): Response
|
public function handle(Request $request, array $params): Response
|
||||||
{
|
{
|
||||||
$csrf = $request->cookies['__csrf'] ?? '';
|
$csrf = $request->cookies['__csrf_token'] ?? '';
|
||||||
$scenarioId = $params['id'] ?? 'new';
|
$scenarioId = $params['id'] ?? 'new';
|
||||||
|
|
||||||
$error = null;
|
$error = null;
|
||||||
|
|||||||
@@ -27,8 +27,17 @@ final class PostStartMatch
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$payload = json_decode($request->rawBody, true, flags: JSON_THROW_ON_ERROR);
|
$payload = json_decode($request->rawBody, true, flags: JSON_THROW_ON_ERROR);
|
||||||
$draft = ScenarioDraft::fromPost($payload);
|
// Accept either the canonical `Scenario` shape (used by the bundled
|
||||||
|
// scenario endpoint at /scenarios/bundled/{id}, where the JS POSTs
|
||||||
|
// the JSON it fetched verbatim) or the editor's `ScenarioDraft`
|
||||||
|
// shape (the form's denormalized fields). The presence of a
|
||||||
|
// nested `battlefield` object distinguishes the two.
|
||||||
|
if (is_array($payload) && is_array($payload['battlefield'] ?? null)) {
|
||||||
|
$scenario = ScenarioSerializer::scenarioFromArray($payload);
|
||||||
|
} else {
|
||||||
|
$draft = ScenarioDraft::fromPost($payload ?? []);
|
||||||
$scenario = $draft->toScenario();
|
$scenario = $draft->toScenario();
|
||||||
|
}
|
||||||
ScenarioValidator::validate($scenario);
|
ScenarioValidator::validate($scenario);
|
||||||
$match = $scenario->startMatch('alpha');
|
$match = $scenario->startMatch('alpha');
|
||||||
} catch (\JsonException $exception) {
|
} catch (\JsonException $exception) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ final class FullFlowTest extends TestCase
|
|||||||
|
|
||||||
// Step 2: POST team editor.
|
// Step 2: POST team editor.
|
||||||
$_COOKIE['__csrf'] = $cookie;
|
$_COOKIE['__csrf'] = $cookie;
|
||||||
|
$_COOKIE['__csrf_token'] = $token;
|
||||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
$_SERVER['REQUEST_URI'] = '/scenarios/demo/edit/team';
|
$_SERVER['REQUEST_URI'] = '/scenarios/demo/edit/team';
|
||||||
$_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
|
$_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
|
||||||
@@ -104,6 +105,7 @@ final class FullFlowTest extends TestCase
|
|||||||
try {
|
try {
|
||||||
[$token, $cookie] = CsrfToken::issue(self::SECRET);
|
[$token, $cookie] = CsrfToken::issue(self::SECRET);
|
||||||
$_COOKIE['__csrf'] = $cookie;
|
$_COOKIE['__csrf'] = $cookie;
|
||||||
|
$_COOKIE['__csrf_token'] = $token;
|
||||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
$_SERVER['REQUEST_URI'] = '/assets/upload';
|
$_SERVER['REQUEST_URI'] = '/assets/upload';
|
||||||
$_SERVER['CONTENT_TYPE'] = 'multipart/form-data; boundary=----test';
|
$_SERVER['CONTENT_TYPE'] = 'multipart/form-data; boundary=----test';
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ final class PostBattlefieldEditorTest extends TestCase
|
|||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
rawBody: json_encode($this->validBattlefield(), JSON_THROW_ON_ERROR),
|
rawBody: json_encode($this->validBattlefield(), JSON_THROW_ON_ERROR),
|
||||||
csrfHeader: $token,
|
csrfHeader: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
@@ -48,7 +48,7 @@ final class PostBattlefieldEditorTest extends TestCase
|
|||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
rawBody: json_encode(['this' => 'is not a valid scenario'], JSON_THROW_ON_ERROR),
|
rawBody: json_encode(['this' => 'is not a valid scenario'], JSON_THROW_ON_ERROR),
|
||||||
csrfHeader: $token,
|
csrfHeader: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ final class PostImageUploadTest extends TestCase
|
|||||||
get: [],
|
get: [],
|
||||||
post: ['_csrf' => $token],
|
post: ['_csrf' => $token],
|
||||||
files: ['image' => ['name' => 'a.png', 'tmp_name' => $tmp, 'error' => 0, 'size' => 70, 'type' => 'image/png']],
|
files: ['image' => ['name' => 'a.png', 'tmp_name' => $tmp, 'error' => 0, 'size' => 70, 'type' => 'image/png']],
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
queryString: '',
|
queryString: '',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -78,7 +78,7 @@ final class PostImageUploadTest extends TestCase
|
|||||||
get: [],
|
get: [],
|
||||||
post: ['_csrf' => $token],
|
post: ['_csrf' => $token],
|
||||||
files: ['image' => ['name' => 'a.png', 'tmp_name' => $tmp, 'error' => 0, 'size' => 12, 'type' => 'image/png']],
|
files: ['image' => ['name' => 'a.png', 'tmp_name' => $tmp, 'error' => 0, 'size' => 12, 'type' => 'image/png']],
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
queryString: '',
|
queryString: '',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -67,7 +67,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -100,7 +100,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -135,7 +135,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -175,7 +175,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -220,7 +220,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -247,7 +247,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $token,
|
turnToken: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -275,7 +275,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $stale,
|
turnToken: $stale,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
@@ -292,7 +292,7 @@ final class PostMatchActionTest extends TestCase
|
|||||||
match: $match,
|
match: $match,
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $wrongRound,
|
turnToken: $wrongRound,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
body: [
|
body: [
|
||||||
'matchId' => self::MATCH_ID,
|
'matchId' => self::MATCH_ID,
|
||||||
'match' => ScenarioSerializer::matchToArray($match),
|
'match' => ScenarioSerializer::matchToArray($match),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ final class PostStartMatchTest extends TestCase
|
|||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
rawBody: json_encode($this->validScenario(), JSON_THROW_ON_ERROR),
|
rawBody: json_encode($this->validScenario(), JSON_THROW_ON_ERROR),
|
||||||
csrfHeader: $token,
|
csrfHeader: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ final class PostStartMatchTest extends TestCase
|
|||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
rawBody: json_encode(['this' => 'is not a valid scenario'], JSON_THROW_ON_ERROR),
|
rawBody: json_encode(['this' => 'is not a valid scenario'], JSON_THROW_ON_ERROR),
|
||||||
csrfHeader: $token,
|
csrfHeader: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ final class PostStartMatchTest extends TestCase
|
|||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
rawBody: json_encode($this->validScenario(), JSON_THROW_ON_ERROR),
|
rawBody: json_encode($this->validScenario(), JSON_THROW_ON_ERROR),
|
||||||
csrfHeader: $token,
|
csrfHeader: $token,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ final class PostTeamEditorTest extends TestCase
|
|||||||
$handler = new PostTeamEditor();
|
$handler = new PostTeamEditor();
|
||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
post: $this->validPost($token),
|
post: $this->validPost($token),
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
@@ -57,7 +57,7 @@ final class PostTeamEditorTest extends TestCase
|
|||||||
$post['teamA']['units'][0]['maxHealth'] = '9999';
|
$post['teamA']['units'][0]['maxHealth'] = '9999';
|
||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
post: $post,
|
post: $post,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => 'demo']);
|
$response = $handler->handle($request, ['id' => 'demo']);
|
||||||
@@ -75,7 +75,7 @@ final class PostTeamEditorTest extends TestCase
|
|||||||
$post['id'] = '</script><script>alert(1)</script>';
|
$post['id'] = '</script><script>alert(1)</script>';
|
||||||
$request = $this->buildRequest(
|
$request = $this->buildRequest(
|
||||||
post: $post,
|
post: $post,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
server: ['__csrf_secret' => self::SECRET],
|
server: ['__csrf_secret' => self::SECRET],
|
||||||
);
|
);
|
||||||
$response = $handler->handle($request, ['id' => $post['id']]);
|
$response = $handler->handle($request, ['id' => $post['id']]);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ final class MatchActionSupportTest extends TestCase
|
|||||||
match: $this->validMatchArray(),
|
match: $this->validMatchArray(),
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: $badToken,
|
turnToken: $badToken,
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $support->verify($request);
|
$result = $support->verify($request);
|
||||||
@@ -66,7 +66,7 @@ final class MatchActionSupportTest extends TestCase
|
|||||||
match: ['this' => 'is not a match'],
|
match: ['this' => 'is not a match'],
|
||||||
csrf: $csrf,
|
csrf: $csrf,
|
||||||
turnToken: 'whatever',
|
turnToken: 'whatever',
|
||||||
cookies: ['__csrf' => $cookie],
|
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $support->verify($request);
|
$result = $support->verify($request);
|
||||||
|
|||||||
Reference in New Issue
Block a user