Skip to content

Commit

Permalink
feat: switch to DateTimeInterface
Browse files Browse the repository at this point in the history
To prevent BC breaks, a new interface and new classes have been created.
While at it, I introduced a SHA256 signer, a lot more secure than the
MD5 one.
  • Loading branch information
alanpoulain committed Apr 19, 2023
1 parent 56fa414 commit cdb9f7a
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 22 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
This package can create URLs with a limited lifetime. This is done by adding an expiration date and a signature to the URL.

```php
$urlSigner = new MD5UrlSigner('randomkey');
$urlSigner = new Sha256UrlSigner('randomkey');

$urlSigner->sign('https://myapp.com', 30);

// => The generated url will be valid for 30 seconds
```

This will output an URL that looks like `https://myapp.com/?expires=xxxx&signature=xxxx`.
This will output a URL that looks like `https://myapp.com/?expires=xxxx&signature=xxxx`.

Imagine mailing this URL out to the users of your application. When a user clicks on a signed URL
your application can validate it with:
Expand Down Expand Up @@ -53,50 +53,50 @@ composer require spatie/url-signer
A signer-object can sign URLs and validate signed URLs. A secret key is used to generate signatures.

```php
use Spatie\UrlSigner\Md5UrlSigner;
use Spatie\UrlSigner\Sha256UrlSigner;

$urlSigner = new Md5UrlSigner('mysecretkey');
$urlSigner = new Sha256UrlSigner('mysecretkey');
```

### Generating URLs

Signed URLs can be generated by providing a regular URL and an expiration date to the `sign` method.

```php
$expirationDate = (new DateTime)->modify('10 days');
$expirationDate = (new DateTime())->modify('10 days');

$urlSigner->sign('https://myapp.com', $expirationDate);

// => The generated url will be valid for 10 days
```

If an integer is provided as expiration date, the url will be valid for that amount of seconds.
If an integer is provided as expiration date, the URL will be valid for that amount of seconds.

```php
$urlSigner->sign('https://myapp.com', 30);

// => The generated url will be valid for 30 seconds
// => The generated URL will be valid for 30 seconds
```

### Validating URLs

To validate a signed URL, simply call the `validate()` method. This will return a boolean.

```php
$urlSigner->validate('https://myapp.com/?expires=1439223344&signature=2d42f65bd023362c6b61f7432705d811');
$urlSigner->validate('https://myapp.com/?expires=1439223344&signature=a479abde194d111022a6831edbda29b14e7bdb760438a8a0be2556cd1a6c23fa');

// => true

$urlSigner->validate('https://myapp.com/?expires=1439223344&signature=2d42f65bd0-INVALID-23362c6b61f7432705d811');
$urlSigner->validate('https://myapp.com/?expires=1439223344&signature=a479abde194d111022a6831edbda-INVALID-29b14e7bdb760438a8a0be2556cd1a6c23fa');

// => false
```

## Writing custom signers

This packages provides a signer that uses md5 to generate signature. You can create your own
signer by implementing the `Spatie\UrlSigner\UrlSigner`-interface. If you let your signer extend
`Spatie\UrlSigner\BaseUrlSigner` you'll only need to provide the `createSignature`-method.
This packages provides a signer that uses SHA256 to generate signature. You can create your own
signer by implementing the `Spatie\UrlSigner\Contracts\UrlSigner`-interface. If you let your signer extend
`Spatie\UrlSigner\AbstractUrlSigner` you'll only need to provide the `createSignature`-method.

## Tests

Expand Down
20 changes: 10 additions & 10 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
backupStaticProperties="false"
cacheDirectory=".phpunit.cache"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<coverage/>
<testsuites>
<testsuite name="spatie Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<source>
<include>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
</include>
</source>
</phpunit>
132 changes: 132 additions & 0 deletions src/AbstractUrlSigner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Spatie\UrlSigner;

use DateTime;
use DateTimeInterface;
use Spatie\UrlSigner\Contracts\UrlSigner as UrlSignerContract;
use Spatie\UrlSigner\Exceptions\InvalidExpiration;
use Spatie\UrlSigner\Exceptions\InvalidSignatureKey;
use Spatie\UrlSigner\Support\Url;

abstract class AbstractUrlSigner implements UrlSignerContract
{
public function __construct(
protected string $defaultSignatureKey,
protected string $expiresParameterName = 'expires',
protected string $signatureParameterName = 'signature'
) {
if ($this->defaultSignatureKey == '') {
throw InvalidSignatureKey::signatureEmpty();
}
}

abstract protected function createSignature(
string $url,
string $expiration,
string $signatureKey,
): string;

public function sign(
string $url,
int|DateTimeInterface $expiration,
string $signatureKey = null,
): string {
$signatureKey ??= $this->defaultSignatureKey;

$expiration = $this->getExpirationTimestamp($expiration);

$signature = $this->createSignature($url, $expiration, $signatureKey);

return $this->signUrl($url, $expiration, $signature);
}

protected function signUrl(string $url, string $expiration, $signature): string
{
return Url::addQueryParameters($url, [
$this->expiresParameterName => $expiration,
$this->signatureParameterName => $signature,
]);
}

public function validate(string $url, string $signatureKey = null): bool
{
$signatureKey ??= $this->defaultSignatureKey;

$queryParameters = Url::queryParameters($url);
if ($this->isMissingAQueryParameter($queryParameters)) {
return false;
}

$expiration = $queryParameters[$this->expiresParameterName];

if (! $this->isFuture($expiration)) {
return false;
}

if (! $this->hasValidSignature($url, $signatureKey)) {
return false;
}

return true;
}

protected function isMissingAQueryParameter(array $query): bool
{
if (! isset($query[$this->expiresParameterName])) {
return true;
}

if (! isset($query[$this->signatureParameterName])) {
return true;
}

return false;
}

protected function isFuture(int $timestamp): bool
{
return $timestamp >= (new DateTime())->getTimestamp();
}

protected function getIntendedUrl(string $url): string
{
return Url::withoutParameters($url, [
$this->expiresParameterName,
$this->signatureParameterName,
]);
}

protected function getExpirationTimestamp(DateTimeInterface|int $expirationInSeconds): string
{
if (is_int($expirationInSeconds)) {
$expirationInSeconds = (new DateTime())->modify($expirationInSeconds.' seconds');
}

if (! $expirationInSeconds instanceof DateTimeInterface) {
throw InvalidExpiration::wrongType();
}

if (! $this->isFuture($expirationInSeconds->getTimestamp())) {
throw InvalidExpiration::isInPast();
}

return (string) $expirationInSeconds->getTimestamp();
}

protected function hasValidSignature(
string $url,
string $signatureKey,
): bool {
$queryParameters = Url::queryParameters($url);

$expiration = $queryParameters[$this->expiresParameterName];
$providedSignature = $queryParameters[$this->signatureParameterName];

$intendedUrl = $this->getIntendedUrl($url);

$validSignature = $this->createSignature($intendedUrl, $expiration, $signatureKey);

return hash_equals($validSignature, $providedSignature);
}
}
3 changes: 3 additions & 0 deletions src/BaseUrlSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Spatie\UrlSigner\Exceptions\InvalidSignatureKey;
use Spatie\UrlSigner\Support\Url;

/**
* @deprecated Use {@see AbstractUrlSigner} instead
*/
abstract class BaseUrlSigner implements UrlSigner
{
public function __construct(
Expand Down
16 changes: 16 additions & 0 deletions src/Contracts/UrlSigner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\UrlSigner\Contracts;

use DateTimeInterface;

interface UrlSigner
{
public function sign(
string $url,
int|DateTimeInterface $expiration,
string $signatureKey = null
): string;

public function validate(string $url, string $signatureKey = null): bool;
}
3 changes: 3 additions & 0 deletions src/Md5UrlSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Spatie\UrlSigner;

/**
* @deprecated Use {@see Sha256UrlSigner} instead
*/
class Md5UrlSigner extends BaseUrlSigner
{
protected function createSignature(
Expand Down
14 changes: 14 additions & 0 deletions src/Sha256UrlSigner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Spatie\UrlSigner;

class Sha256UrlSigner extends AbstractUrlSigner
{
protected function createSignature(
string $url,
string $expiration,
string $signatureKey
): string {
return hash_hmac('sha256', "{$url}::{$expiration}", $signatureKey);
}
}
3 changes: 3 additions & 0 deletions src/UrlSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use DateTime;

/**
* @deprecated Use {@see \Spatie\UrlSigner\Contracts\UrlSigner} instead
*/
interface UrlSigner
{
public function sign(
Expand Down
Loading

0 comments on commit cdb9f7a

Please sign in to comment.