> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling signed URL input correctly

> Learn what Laravel 13 signed URLs protect and why you should retrieve signed query values with query(), using a time-limited link as an example.

## Purpose of this page

When you want a time-limited share link to use exactly the conditions set when it was issued, you need to do more than verify the URL signature. You also need to be consistent about **where you read the values from**.
This page walks through an example that includes a report type and output format in the URL and returns preview JSON.

This page covers Laravel 13. For URL generation in general, see [URL generation](/en/urls). For the basics of retrieving input, see [HTTP requests](/en/requests).

<Warning>
  A signature does not protect the entire request. Form and JSON request bodies are not signed.
  After the signature is verified, retrieve signed query values using methods such as `$request->query('format')`.
  Methods such as `input()` and `all()`, as well as dynamic properties, may return unsigned body values. This distinction matters for GET requests too.
</Warning>

## What the signature protects

| Element | Treatment |
| - | - |
| URL path | Covered by signature verification |
| Query string | Covered by signature verification, except for parameters you exclude from verification |
| Scheme and host | Normally included in the signature. Not included in relative signatures |
| Form and JSON request bodies | Not covered by the signature |
| Expiration time | Added by `temporarySignedRoute()` and checked for expiration during verification |

`input()` is an API that reads not only the query string but also the request body. When Laravel merges these inputs, body values take precedence over query string values.
`query()`, on the other hand, reads only the query string.

A successful signature check and the fact that a value you read afterward is signed are two separate things to verify.

```mermaid theme={null}
flowchart TD
    A["Access via time-limited URL"] --> B{"signed middleware<br>Verify signature and expiration"}
    B -->|Invalid| C["403 response"]
    B -->|Valid| D["Retrieve signed query values<br>with query()"]
    D --> E["Validate value format and allowed range"]
    E --> F["Return preview"]
```

## Implementing a time-limited preview link

### 1. Define the route to verify

In `routes/web.php`, define a named route that requires a signature.
This example does not touch the database or files. It simply returns the conditions it received.

```php theme={null}
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;

Route::get('/reports/preview', function (Request $request) {
    $options = Validator::make([
        'report' => $request->query('report'),
        'format' => $request->query('format'),
    ], [
        'report' => ['required', 'string', 'in:monthly,annual'],
        'format' => ['required', 'string', 'in:csv,pdf'],
    ])->validate();

    return response()->json($options);
})->middleware('signed')->name('reports.preview');
```

* The `signed` middleware verifies the signature and expiration before the closure runs. If either is invalid, it returns `403`.
* `query()` explicitly reads the values used for processing from the query string.
* Validation runs against the retrieved array. Signature verification does not guarantee value types or business-level allowed ranges.

<Info>
  In this example, only the values extracted with `query()` are passed to `Validator::make()`.
  If you replace them with `$request->all()` in code meant to validate signed query values, you end up handling input that includes the request body.
</Info>

### 2. Generate the URL in a trusted issuing process

After registering the named route, use the following code in the process that issues links.
Because `report` and `format` are not path placeholders, they are appended to the query string.

```php theme={null}
use Illuminate\Support\Facades\URL;

$url = URL::temporarySignedRoute(
    'reports.preview',
    now()->addMinutes(30),
    ['report' => 'monthly', 'format' => 'csv'],
);
```

In addition to the conditions you specified, this URL contains `expires` and `signature`.
`expires` and `signature` are reserved parameters used by Laravel, so don't pass them as names for your own business parameters.

If you expose the issuing process in a real application, don't let anyone issue links with arbitrary conditions. Apply the necessary [authentication](/en/authentication) and [authorization](/en/authorization).

### 3. Check normal usage

If you open the issued URL as is within its validity period, the following JSON is returned.

```json theme={null}
{
  "report": "monthly",
  "format": "csv"
}
```

Once the link expires, the route's `signed` middleware returns `403`.
Signed URLs are not single-use tokens. In this example, the same URL can be reused while it is still valid.
If you want a link to be used only once, you need to track its used state separately from the signature.

## Separating values excluded from verification

If you want to allow values such as page numbers added by the frontend, you can exclude them from signature verification with `hasValidSignatureWhileIgnoring(['page'])`.
However, anyone can change excluded values, so don't treat them as signed conditions.

For example, `page` for display purposes plays a different role from `report` and `format`, which are fixed at issuance.
Don't exclude values used to decide the target data, recipients, pricing, and so on just for convenience.

<Warning>
  The example above uses the standard `signed` middleware, so it does not exclude `page` either.
  If you want to add query parameters, decide first what the generating side and the verifying side will treat as signed.
</Warning>

## Using relative signatures

If your design does not include the domain in the signature, combine `absolute: false` at generation time with `signed:relative` at verification time.

```php theme={null}
$url = URL::temporarySignedRoute(
    'reports.preview',
    now()->addMinutes(30),
    ['report' => 'monthly', 'format' => 'csv'],
    absolute: false,
);
```

In this case, change `middleware('signed')` in the earlier route to `middleware('signed:relative')`.
Request bodies are not signed with relative signatures either, so you should still retrieve values with `query()`.

## Implementation checklist

* The receiving route requires signature verification.
* Signed query values are retrieved with `query()`.
* Value format and allowed ranges are validated separately.
* Values excluded from signature verification are not used as conditions fixed at issuance.
* Absolute and relative signature settings match on the generating and verifying sides.
* Knowing the URL is not treated as equivalent to the logged-in user's permissions.

## Related pages and primary sources

* [URL generation](/en/urls): Signed URL APIs and responses to invalid signatures
* [HTTP requests](/en/requests): Retrieving input values and query strings
* [Validation](/en/validation): Validating input format and allowed values
* [Laravel official documentation: Signed URLs](https://github.com/laravel/docs/blob/13.x/urls.md#signed-urls)
* [Laravel official documentation: Retrieving input from the query string](https://github.com/laravel/docs/blob/13.x/requests.md#retrieving-input-from-the-query-string)
* [Laravel Framework: UrlGenerator](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Routing/UrlGenerator.php)
* [Laravel Framework: ValidateSignature](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Routing/Middleware/ValidateSignature.php)


## Related topics

- [URL generation](/en/urls.md)
- [Guide to Building Apps with VOICEVOX Engine API](/en/packages/laravel-voicevox/app-guide.md)
- [HTTP requests](/en/requests.md)
- [Laravel Sail](/en/sail.md)
- [Labeler](/en/packages/laravel-bluesky/labeler.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.