Example: authorization and rate limit
Four endpoints with the same handler, each protected differently.
| Endpoint | Protection | Try |
|---|---|---|
/api/v1/bearer | BearerTokenAuthorization | -H 'Authorization: Bearer secret-token' |
/api/v1/api-key | HeaderApiKeyAuthentication | -H 'X-API-Key: secret-token' |
/api/v1/basic | BasicCredentialsAuthorization from the example | -u admin:change-me |
/api/v1/limited | NoAuthorization and a FixedWindowLimiter with NetteCacheStorage | four calls in a minute |
The services
application:
mapping:
Api: Tomaj\NetteApi\Presenters\*Presenter
parameters:
# token => allowed IPs: '*' for any, one IP, a list, or an IPv4 CIDR range
apiTokens:
secret-token: '*'
office-token: '10.0.0.0/8'
apiUsers:
admin: change-me
services:
router: App\RouterFactory::create
apiPresenter:
factory: Tomaj\NetteApi\Presenters\ApiPresenter
- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
- Tomaj\NetteApi\Error\DefaultErrorHandler
# the only IpDetectorInterface service, so it is autowired everywhere; it cannot be forged with headers
- Tomaj\NetteApi\Misc\IpDetector
tokens: Tomaj\NetteApi\Misc\StaticTokenRepository(%apiTokens%)
bearer: Tomaj\NetteApi\Authorization\BearerTokenAuthorization(@tokens)
apiKey: Tomaj\NetteApi\Authorization\HeaderApiKeyAuthentication('X-API-Key', @tokens)
basic: App\BasicCredentialsAuthorization(%apiUsers%)
# 3 requests per minute per IP, counted in the Nette cache (%tempDir%/cache)
rateLimit:
factory: Tomaj\NetteApi\RateLimit\FixedWindowLimiter(perIp, 3, 60, Tomaj\NetteApi\RateLimit\Key\IpKey(), Tomaj\NetteApi\RateLimit\Storage\NetteCacheStorage())
- App\Api\WhoAmIHandler
apiDecider:
factory: Tomaj\NetteApi\ApiDecider
setup:
# Authorization: Bearer secret-token
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'bearer'), App\Api\WhoAmIHandler, @bearer)
# X-API-Key: secret-token
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'api-key'), App\Api\WhoAmIHandler, @apiKey)
# curl -u admin:change-me
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'basic'), App\Api\WhoAmIHandler, @basic)
# no authorization, 3 requests per minute per IP
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'limited'), App\Api\WhoAmIHandler, Tomaj\NetteApi\Authorization\NoAuthorization(), @rateLimit)office-token is allowed only from 10.0.0.0/8; the test shows that a forged X-Forwarded-For does not help.
The client IP
HTTP Basic
<?php
declare(strict_types=1);
namespace App;
use Nette\Http\IRequest;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
/**
* HTTP Basic authorization that reads the Authorization header.
*
* The bundled BasicAuthentication reads the user and password from the request URL,
* which nette/http fills only up to version 3.1. Since nette/http 3.2 the URL has no
* credentials, so use a class like this one, or install nette/http 3.1.
* Nette API 4.0 reads the header in BasicAuthentication, so there this class is not needed.
*/
final class BasicCredentialsAuthorization implements ApiAuthorizationInterface
{
/**
* @param array<string, string> $users username => password
*/
public function __construct(private array $users, private IRequest $request)
{
}
public function authorized(): bool
{
$credentials = $this->request->getBasicCredentials();
if ($credentials === null) {
return false;
}
[$user, $password] = $credentials;
// hash_equals compares in constant time, so the response time leaks nothing
return isset($this->users[$user]) && hash_equals($this->users[$user], $password);
}
public function getErrorMessage(): ?string
{
return 'Incorrect username or password';
}
}In 3.x the bundled BasicAuthentication does not work with nette/http 3.2 and newer (4.0 fixes it); this class reads the Authorization header and compares with hash_equals().
The test
#!/usr/bin/env bash
# End-to-end check: each authorization passes with the right credentials and answers 401 without them.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
rm -rf temp/cache
serve 8083
request -H 'Authorization: Bearer secret-token' "$BASE/api/v1/bearer"
expect_status 200 "bearer token"
expect_body '"endpoint":"v1/bearer/"' "bearer token"
ok "a valid bearer token passes"
request "$BASE/api/v1/bearer"
expect_status 401 "no bearer token"
expect_body '"message":"Authorization header HTTP_Authorization is not set"' "no bearer token"
ok "no Authorization header answers 401"
request -H 'Authorization: Bearer wrong' "$BASE/api/v1/bearer"
expect_status 401 "unknown bearer token"
expect_body "\"message\":\"Token doesn't exists or isn't active\"" "unknown bearer token"
ok "an unknown token answers 401"
request -H 'Authorization: Bearer office-token' -H 'X-Forwarded-For: 10.1.2.3' "$BASE/api/v1/bearer"
expect_status 401 "token from a wrong IP"
expect_body '"message":"Invalid IP"' "token from a wrong IP"
ok "an IP restricted token from 127.0.0.1 answers 401, a forged X-Forwarded-For does not help"
request -H 'X-API-Key: secret-token' "$BASE/api/v1/api-key"
expect_status 200 "api key"
ok "a valid X-API-Key passes"
request "$BASE/api/v1/api-key"
expect_status 401 "no api key"
expect_body '"message":"API key is not set"' "no api key"
ok "no X-API-Key answers 401"
request -u admin:change-me "$BASE/api/v1/basic"
expect_status 200 "basic auth"
ok "HTTP Basic with the right password passes"
request -u admin:wrong "$BASE/api/v1/basic"
expect_status 401 "basic auth wrong password"
expect_body '"message":"Incorrect username or password"' "basic auth wrong password"
ok "HTTP Basic with a wrong password answers 401"
avoid_minute_edge
for i in 1 2 3; do
request "$BASE/api/v1/limited"
expect_status 200 "rate limit request $i"
done
expect_header 'RateLimit-Policy: "perIp";q=3;w=60' "rate limit headers"
expect_header 'RateLimit: "perIp";r=0;' "rate limit headers"
request "$BASE/api/v1/limited"
expect_status 429 "rate limit exceeded"
expect_header 'Retry-After:' "rate limit exceeded"
expect_body '"message":"Too many requests. Retry after' "rate limit exceeded"
ok "the fourth request in a minute answers 429 with Retry-After"FAQ
Where do real tokens come from?
From your own TokenRepositoryInterface backed by a database. StaticTokenRepository with tokens in NEON is for examples and internal tools.
How does the rate limit get to an endpoint?
addApi() gets it as the 4th argument, a RateLimiterInterface is not autowired. See Rate limiting.
What do the 401 messages say?
For example Token doesn't exists or isn't active or Invalid IP. See Authorization.
Is the handler different per endpoint?
No, WhoAmIHandler returns the endpoint it was called for, so the test can see which one ran.
Can I try these in the web console?
The OpenAPI and console example has a bearer protected endpoint in the console.