Skip to content

Authorization ​

Every endpoint has an authorization object, the third argument of addApi(). It implements ApiAuthorizationInterface with two methods: authorized(): bool and getErrorMessage(): ?string.

Open the diagram at full size

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 ​

ClassReads the token fromConstructor
NoAuthorizationnothing, always passesnone
BearerTokenAuthorizationAuthorization: Bearer <token>(TokenRepositoryInterface, IpDetectorInterface)
HeaderApiKeyAuthenticationa header, for example X-API-Key(string $headerName, TokenRepositoryInterface, IpDetectorInterface)
QueryApiKeyAuthenticationa query param, for example ?api_key=(string $queryParamName, TokenRepositoryInterface, IpDetectorInterface)
CookieApiKeyAuthenticationa cookie(string $cookieName, TokenRepositoryInterface, IpDetectorInterface)
BasicAuthenticationthe 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:

yaml
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)
RestrictionMeans
* or empty stringany IP
1.2.3.4exactly this IP
1.2.3.4, 5.6.7.8any of the listed, separated by commas, spaces or new lines
10.0.0.0/8an IPv4 range
nullnever, Invalid IP

For a real app, implement TokenRepositoryInterface on top of your database.

Known limits in 3.x

  • IP detection can be spoofed. The IpDetector of 3.x prefers the Client-IP and X-Forwarded-For headers, which any client can send. Since 4.0 it takes the address from Nette\Http\IRequest::getRemoteAddress(), which trusts forwarded headers only from the proxies in http: 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.1 behind 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 /, so 10.0.0.0/8, 192.168.0.0/16 never matches the second range. Put the range last, or one range per token.
  • BasicAuthentication reads the user from the request URL, which nette/http 3.2 and newer leaves empty, so in 3.x it never passes there. Write a class that reads $request->getBasicCredentials(). 4.0 reads the Authorization: Basic header.
  • 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
<?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.

Released under the MIT License.