Authorization
Every endpoint has an authorization object, the third argument of addApi(). It implements ApiAuthorizationInterface with two methods: authorized(): bool and getErrorMessage(): ?string.
When authorized() returns false, the error handler answers 401 with {"status":"error","message":"..."} and the message from getErrorMessage(). An exception thrown from authorized() answers 500 with the exception message.
Built-in classes
| Class | Reads the token from | Constructor |
|---|---|---|
NoAuthorization | nothing, always passes | none |
BearerTokenAuthorization | Authorization: Bearer <token> | (TokenRepositoryInterface, IpDetectorInterface) |
HeaderApiKeyAuthentication | a header, for example X-API-Key | (string $headerName, TokenRepositoryInterface, IpDetectorInterface) |
QueryApiKeyAuthentication | a query param, for example ?api_key= | (string $queryParamName, TokenRepositoryInterface, IpDetectorInterface) |
CookieApiKeyAuthentication | a cookie | (string $cookieName, TokenRepositoryInterface, IpDetectorInterface) |
BasicAuthentication | the Authorization: Basic header (in 3.x the request URL, see below) | (array $users, IRequest $httpRequest) |
The token classes share TokenAuthorization: the token must be valid in the repository, then the client IP must pass the token's IP restriction.
Tokens and IP restrictions
TokenRepositoryInterface has validToken(string $token): bool and ipRestrictions(string $token): ?string. StaticTokenRepository takes an array of token => restriction:
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)| Restriction | Means |
|---|---|
* or empty string | any IP |
1.2.3.4 | exactly this IP |
1.2.3.4, 5.6.7.8 | any of the listed, separated by commas, spaces or new lines |
10.0.0.0/8 | an IPv4 range |
null | never, Invalid IP |
For a real app, implement TokenRepositoryInterface on top of your database.
Known limits in 3.x
- IP detection can be spoofed. The
IpDetectorof 3.x prefers theClient-IPandX-Forwarded-Forheaders, which any client can send. Since 4.0 it takes the address fromNette\Http\IRequest::getRemoteAddress(), which trusts forwarded headers only from the proxies inhttp: proxy:, so behind a reverse proxy list it there, otherwise every client has the address of the proxy. A token restricted to the address of the proxy (127.0.0.1behind a local nginx, a range of your load balancer) then passes for every client that has the token. See the upgrade guide. - Only the first range counts. In a list, checking stops at the first entry with a
/, so10.0.0.0/8, 192.168.0.0/16never matches the second range. Put the range last, or one range per token. - BasicAuthentication reads the user from the request URL, which
nette/http3.2 and newer leaves empty, so in 3.x it never passes there. Write a class that reads$request->getBasicCredentials(). 4.0 reads theAuthorization: Basicheader. - Numeric tokens never match in
StaticTokenRepository: PHP turns a key like'12345'into an integer, which is never equal to the string token. Use keys with a letter.
Since 3.6.0, BasicAuthentication and StaticTokenRepository compare passwords and tokens with hash_equals(), in constant time, so the response time does not reveal how much of a token was right. Which values match is the same as before. In your own token repository or authorization, compare secrets with hash_equals() too.
Your own authorization
<?php
declare(strict_types=1);
namespace App\Api;
use Nette\Security\User;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
final class AdminOnlyAuthorization implements ApiAuthorizationInterface
{
public function __construct(private User $user) {}
public function authorized(): bool
{
return $this->user->isInRole('admin');
}
public function getErrorMessage(): ?string
{
return 'Admins only';
}
}For a rate limit with key: identity, implement AuthorizationIdentityInterface too: getIdentity() returns who called (a user id, a token), or null before a successful authorized(). The built-in token authorizations return the token and BasicAuthentication the user name. A subclass that overrides their authorized() without calling the parent must override getIdentity() too. See Rate limiting.
FAQ
What does the 401 body look like?
{"status":"error","message":"Token doesn't exists or isn't active"}, or another message from getErrorMessage(). You can change it in your own error handler.
Can one endpoint accept a bearer token or an API key?
Write a class that tries both: call authorized() on each and remember which message to return. The authorization example shows each method on its own endpoint.
Does the console send my token?
Yes, the web console has a field for the bearer token, for header, query and cookie API keys, and user and password for Basic.
How do I tell OpenAPI that an endpoint needs a token?
OpenApiHandler reads the authorization class and adds the matching security scheme. See OpenAPI.
Where do I limit how often a token may call the API?
That is a rate limit, set per endpoint next to the authorization.