Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: bring readme up to date #173

Merged
merged 1 commit into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 26 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,36 @@ The CSRF library does two things:

Each is just a single method call, but you need to set up first.

### Step 1: Set Up
### Step 1: Set up

Start by creating the TokenStore. There is currently a single implementation — the `ArrayTokenStore`. Because the `ArrayTokenStore` is not persistent, you need to save it between page requests so that tokens generated for one page request can be checked on another. The easiest way to save it is to put it on the `Session`:
Start by creating the TokenStore. There are currently two implementations — the `ArrayTokenStore` and `SessionTokenStore`. The `ArrayTokenStore` is the most basic and does not persist in any way, but can be extended into custom integrations. The `SessionTokenStore` is an inbuilt implementation that persists tokens between requests, so that tokens generated for one page request can be checked on another. The easiest way to add CSRF protection is to use the Session:

```php
use Gt\Csrf\ArrayTokenStore;
use Gt\Csrf\SessionTokenStore;

// check to see if there's already a token store for this session, and
// create one if not
if(!$session->contains("gt.csrf")) {
$session->set("gt.csrf", new ArrayTokenStore());
}

$tokenStore = $session->get("gt.csrf");
// $session is an object-oriented representation of $_SESSION
// that implements the Gt\Session\SessionContainer Interface.
$tokenStore = new SessionTokenStore($session);
```

### Step 2: Verify

Before running any other code (especially things that could affect data), you should check to make sure that there's a valid CSRF token in place if it's needed. That step is also very straightforward:
Before running any other code (especially things that could affect data), you should check to make sure that there's a valid CSRF token in place if it's needed:

```php
use Gt\Csrf\Exception\CSRFException;

try {
$tokenStore->processAndVerify();
}
catch(CSRFException $e) {
if(this_is_a_post_request()) {
try {
$tokenStore->verify();
}
catch(CSRFException $e) {
// Stop processing this request and get out of there!
}
}
```

If the request contains a POST and there is no valid CSRF token, a `CSRFException` will be thrown — so you should plan to catch it. Remember, if that happens, the request was fraudulent so you shouldn't process it!
If the request contains a POST and there is no valid CSRF token, a `CSRFException` will be thrown — so you should plan to catch it. Remember, if that happens, the request was fraudulent, so you shouldn't process it!

### Step 3: Inject for Next Time

Expand All @@ -76,28 +74,29 @@ use Gt\Csrf\HTMLDocumentProtector;
$html = "<html>...</html>";

// Now do the processing.
$page = new HTMLDocumentProtector($html, $tokenStore);
$page->protectAndInject();
$protector = new HTMLDocumentProtector($html, $tokenStore);
$protector->protect();

// You can get it back out however you wish.
echo $page->getHTMLDocument()->saveHTML();
// Output the HTML of the document - you will see the new fields have
// been automatically injected.
echo $protector->getHTMLDocument();
```

Using Tokens of a Different Length
Using tokens of a fifferent length
----------------------------------

By default, 32 character tokens are generated. They use characters from the set [a-zA-Z0-9], meaning a 64-bit token which would take a brute-force attacker making 100,000 requests per second around 2.93 million years to guess. If this seems either excessive or inadequate you can change the token length using `TokenStore::setTokenLength()`.

Special Note About AJAX Clients
-------------------------------
Special note about client-side requests
---------------------------------------

Note that if there are several forms on your page, a unique token will be generated and injected into each form. When a form is submitted using AJAX, the response will contain a new token that must be refreshed in the page ready for the next submission.
Note that if there are several forms on your page, a unique token will be generated and injected into each form. When a form is submitted using a client-side request (XMLHTTPRequest or Fetch, a.k.a. AJAX), the response will contain a new token that must be refreshed in the page ready for the next submission.

If you would prefer to have one token per page, shared across all forms, this can be configured by passing in the TOKEN_PER_PAGE parameter to the projectAndInject method: `$page->protectAndInject(HTMLDocumentProtector::TOKEN_PER_PAGE);`.

Storing one token per page will reduce the amount of server resources required, but concurrent AJAX requests will fail which is why one token per form is the default.
Storing one token per page will reduce the amount of server resources required, but concurrent client-side requests will fail, which is why one token per form is the default.

Alternatives to Storing Tokens on the Session
Alternatives to storing tokens on the session
---------------------------------------------

The package includes an `ArrayTokenStore`, which can be stored on the session. You can implement alternative token stores such as a RDBMS or Mongo by subclassing `TokenStore` and implementing the abstract methods.
The package includes an `ArrayTokenStore`, which can be stored on the session. You can implement alternative token stores such as a RDBMS or NoSQL by subclassing `TokenStore` and implementing the abstract methods.
2 changes: 1 addition & 1 deletion src/HTMLDocumentProtector.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function __construct(
* generating unique tokens. In most cases this default is suitable - wherever the normal
* model of returning a new page in response to a form submit is used.
*/
public function protectAndInject(
public function protect(
string $tokenSharing = self::ONE_TOKEN_PER_PAGE
):string {
$forms = $this->document->forms;
Expand Down
9 changes: 4 additions & 5 deletions src/SessionTokenStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@

use Gt\Csrf\Exception\CsrfTokenInvalidException;
use Gt\Csrf\Exception\CsrfTokenSpentException;
use Gt\Session\SessionStore;
use Gt\Session\SessionContainer;

class SessionTokenStore extends TokenStore {
const SESSION_KEY = "tokenList";

/** @var SessionStore */
protected $session;
protected SessionContainer $session;

public function __construct(
SessionStore $session,
SessionContainer $session,
int $maxTokens = null
) {
$this->session = $session;
Expand Down Expand Up @@ -51,4 +50,4 @@ public function consumeToken(string $token):void {
$tokenList[$token] = time();
$this->session->set(self::SESSION_KEY, $tokenList);
}
}
}
2 changes: 1 addition & 1 deletion src/TokenStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function generateNewToken():string {
* $_POST but it has already been consumed by a previous request.
* @see TokenStore::verifyToken().
*/
public function processAndVerify(array|object $postData):void {
public function verify(array|object $postData):void {
// Expect the token to be present on ALL post requests.
if(!is_array($postData)
&& is_callable([$postData, "asArray"])) {
Expand Down
32 changes: 16 additions & 16 deletions test/phpunit/HTMLDocumentProtectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ public function testConstruct_fromDomDocument():void {
);
}

public function testProtectAndInject_zeroForms():void {
public function testProtect_zeroForms():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::NO_FORMS),
new ArrayTokenStore()
);
$sut->protectAndInject();
$sut->protect();

$document = $sut->getHTMLDocument();
$nodeList = $document->querySelectorAll(
Expand All @@ -133,12 +133,12 @@ public function testProtectAndInject_zeroForms():void {
self::assertNull($document->querySelector("form"));
}

public function testProtectAndInject_singleForm():void {
public function testProtect_singleForm():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::ONE_FORM),
new ArrayTokenStore()
);
$sut->protectAndInject();
$sut->protect();

// check that the token has been injected
$document = $sut->getHTMLDocument();
Expand All @@ -162,12 +162,12 @@ public function testProtectAndInject_singleForm():void {
);
}

public function testProtectAndInject_multipleForms():void {
public function testProtect_multipleForms():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::THREE_FORMS),
new ArrayTokenStore()
);
$sut->protectAndInject();
$sut->protect();

// check that the token has been injected in all POST forms (not GET)
$document = $sut->getHTMLDocument();
Expand All @@ -184,12 +184,12 @@ public function testProtectAndInject_multipleForms():void {
);
}

public function testProtectAndInject_singleCodeSharedAcrossForms():void {
public function testProtect_singleCodeSharedAcrossForms():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::THREE_FORMS),
new ArrayTokenStore()
);
$sut->protectAndInject();
$sut->protect();

$document = $sut->getHTMLDocument();
$token = null;
Expand All @@ -206,12 +206,12 @@ public function testProtectAndInject_singleCodeSharedAcrossForms():void {
self::assertEquals($token, $metaTag->content);
}

public function testProtectAndInject_uniqueCodePerForm():void {
public function testProtect_uniqueCodePerForm():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::THREE_FORMS),
new ArrayTokenStore()
);
$sut->protectAndInject(HTMLDocumentProtector::ONE_TOKEN_PER_FORM);
$sut->protect(HTMLDocumentProtector::ONE_TOKEN_PER_FORM);

$document = $sut->getHTMLDocument();
$metaTag = $document->querySelector("head meta[name='" . HTMLDocumentProtector::TOKEN_NAME . "']");
Expand All @@ -226,25 +226,25 @@ public function testProtectAndInject_uniqueCodePerForm():void {
}
}

public function testProtectAndInject_metaTagNoHead():void {
public function testProtect_metaTagNoHead():void {
$sut = new HTMLDocumentProtector(
new HTMLDocument(self::NO_HEAD),
new ArrayTokenStore()
);
$sut->protectAndInject();
$sut->protect();

$document = $sut->getHTMLDocument();
$metaTag = $document->querySelector("head meta[name='" . HTMLDocumentProtector::TOKEN_NAME . "']");
self::assertNotNull($metaTag);
self::assertNotEmpty($metaTag->content);
}

public function testProtectAndInject_metaTagAlreadyExists():void {
public function testProtect_metaTagAlreadyExists():void {
$document = new HTMLDocument(self::HAS_META_ALREADY);
$metaTag = $document->querySelector("head meta[name='" . HTMLDocumentProtector::TOKEN_NAME . "']");
$originalValue = $metaTag->content;
$sut = new HTMLDocumentProtector($document, new ArrayTokenStore());
$sut->protectAndInject();
$sut->protect();

$metaTag = $document->querySelector("head meta[name='" . HTMLDocumentProtector::TOKEN_NAME . "']");
self::assertNotNull($metaTag);
Expand All @@ -253,10 +253,10 @@ public function testProtectAndInject_metaTagAlreadyExists():void {
self::assertNotEquals($originalValue, $metaTag->content);
}

public function testProtectAndInject_differentTokenName() {
public function testProtect_differentTokenName() {
$sut = new HTMLDocumentProtector(new HTMLDocument(self::HAS_META_ALREADY), new ArrayTokenStore());
$tokenName = HTMLDocumentProtector::TOKEN_NAME;
$sut->protectAndInject();
$sut->protect();

// check that the token has been injected in all forms
$document = $sut->getHTMLDocument();
Expand Down
24 changes: 12 additions & 12 deletions test/phpunit/TokenStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,39 @@ class TokenStoreTest extends TestCase {
HTML;

/** no post request received */
public function testProcessAndVerify_noPost():void {
public function testVerify_noPost():void {
$exception = null;

try {
$sut = new ArrayTokenStore();
$sut->processAndVerify([]);
$sut->verify([]);
}
catch(CsrfException $exception) {}

self::assertNull($exception);
}

/** POST request received but without a token */
public function testProcessAndVerify_noToken():void {
public function testVerify_noToken():void {
$post = [];
$post["doink"] = "binky";
$sut = new ArrayTokenStore();
$this->expectException(CSRFTokenMissingException::class);
$sut->processAndVerify($post);
$sut->verify($post);
}

/** POST request received with token but invalid */
public function testProcessAndVerify_invalidToken():void {
public function testVerify_invalidToken():void {
$post = [];
$post["doink"] = "binky";
$post[HTMLDocumentProtector::TOKEN_NAME] = "12321";
$sut = new ArrayTokenStore();
$this->expectException(CSRFTokenInvalidException::class);
$sut->processAndVerify($post);
$sut->verify($post);
}

/** POST request received with token but invalid */
public function testProcessAndVerify_spentToken():void {
public function testVerify_spentToken():void {
$tokenStore = new ArrayTokenStore();
$token = $tokenStore->generateNewToken();
$tokenStore->saveToken($token);
Expand All @@ -76,11 +76,11 @@ public function testProcessAndVerify_spentToken():void {
$post[HTMLDocumentProtector::TOKEN_NAME] = $token;

$this->expectException(CSRFTokenSpentException::class);
$tokenStore->processAndVerify($post);
$tokenStore->verify($post);
}

/** POST request received with token and valid */
public function testProcessAndVerify_validToken():void {
public function testVerify_validToken():void {
$tokenStore = new ArrayTokenStore();
$token = $tokenStore->generateNewToken();
$tokenStore->saveToken($token);
Expand All @@ -93,7 +93,7 @@ public function testProcessAndVerify_validToken():void {
$exception = null;

try {
$tokenStore->processAndVerify($post);
$tokenStore->verify($post);
}
catch(CsrfException $exception) {}

Expand All @@ -104,7 +104,7 @@ public function testProcessAndVerify_validToken():void {
* php.gt/webengine provides user input as a custom object
* with an asArray function.
*/
public function testProcessAndVerify_validTokenObj():void {
public function testVerify_validTokenObj():void {
$tokenStore = new ArrayTokenStore();
$token = $tokenStore->generateNewToken();
$tokenStore->saveToken($token);
Expand All @@ -124,7 +124,7 @@ public function testProcessAndVerify_validTokenObj():void {
$exception = null;

try {
$tokenStore->processAndVerify($post);
$tokenStore->verify($post);
}
catch(CsrfException $exception) {}

Expand Down