Príklad: autorizácia a obmedzenie požiadaviek
Štyri endpointy s rovnakým handlerom, každý chránený inak.
| Endpoint | Ochrana | Vyskúšajte |
|---|---|---|
/api/v1/bearer | BearerTokenAuthorization | -H 'Authorization: Bearer secret-token' |
/api/v1/api-key | HeaderApiKeyAuthentication | -H 'X-API-Key: secret-token' |
/api/v1/basic | BasicCredentialsAuthorization z príkladu | -u admin:change-me |
/api/v1/limited | NoAuthorization a FixedWindowLimiter s NetteCacheStorage | štyri volania za minútu |
Služby
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 je povolený len z 10.0.0.0/8; test ukazuje, že podvrhnutá X-Forwarded-For nepomôže.
IP klienta
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';
}
}V 3.x vstavaná BasicAuthentication nefunguje s nette/http 3.2 a novším (4.0 to opravuje); táto trieda číta hlavičku Authorization a porovnáva cez hash_equals().
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"Časté otázky
Odkiaľ sa berú skutočné tokeny?
Z vášho vlastného TokenRepositoryInterface nad databázou. StaticTokenRepository s tokenmi v NEON je pre príklady a interné nástroje.
Ako sa obmedzenie dostane k endpointu?
addApi() ho dostane ako štvrtý argument, RateLimiterInterface sa neautowiruje. Pozrite si Obmedzenie požiadaviek.
Čo hovoria hlášky 401?
Napríklad Token doesn't exists or isn't active alebo Invalid IP. Pozrite si Autorizáciu.
Líši sa handler podľa endpointu?
Nie, WhoAmIHandler vráti endpoint, pre ktorý bol zavolaný, takže test vidí, ktorý bežal.
Môžem to vyskúšať vo webovej konzole?
Príklad OpenAPI a konzoly má v konzole endpoint chránený bearer tokenom.