Príklad: vlastná autorizácia
Tri endpointy chránené autorizačnými triedami napísanými pre aplikáciu: ľubovoľný používateľ, iba administrátori a používateľ alebo API kľúč partnera.
| Požiadavka | Odpoveď |
|---|---|
GET /api/v1/me bez tokenu | 401 Missing bearer token |
GET /api/v1/me s Bearer bob-token | 200 {"name":"Bob","roles":["staff"]} |
GET /api/v1/report s Bearer bob-token | 401 Role admin is required |
GET /api/v1/report s Bearer alice-token | 200 |
GET /api/v1/any/me s X-API-Key: partner-key | 200, klient s API kľúčom |
Služby
Každá autorizácia je samostatná služba s autowired: false, takže jedna trieda slúži viacerým endpointom s rôznymi nastaveniami.
application:
mapping:
Api: Tomaj\NetteApi\Presenters\*Presenter
parameters:
# token => user; in a real app this comes from the database
users:
alice-token: {name: Alice, roles: [admin, staff]}
bob-token: {name: Bob, roles: [staff]}
apiKeys:
partner-key: '*'
services:
router: App\RouterFactory::create
apiPresenter:
factory: Tomaj\NetteApi\Presenters\ApiPresenter
- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
- Tomaj\NetteApi\Error\DefaultErrorHandler
- Tomaj\NetteApi\Misc\StaticIpDetector('127.0.0.1')
- App\CurrentUser
- App\Api\MeHandler
- App\Api\ReportHandler
# every authorization is its own service, so one class can be used with different settings
anyUser:
factory: App\RoleTokenAuthorization(%users%)
autowired: false
adminOnly:
factory: App\RoleTokenAuthorization(%users%, requiredRole: admin)
autowired: false
partnerKey:
factory: Tomaj\NetteApi\Authorization\HeaderApiKeyAuthentication('X-API-Key', Tomaj\NetteApi\Misc\StaticTokenRepository(%apiKeys%))
autowired: false
userOrPartner:
factory: App\AnyOfAuthorization(@anyUser, @partnerKey)
autowired: false
apiDecider:
factory: Tomaj\NetteApi\ApiDecider
setup:
# any valid bearer token
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'me'), App\Api\MeHandler, @anyUser)
# bearer token of a user with the admin role
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'report'), App\Api\ReportHandler, @adminOnly)
# a bearer token or the X-API-Key header
- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'me', 'any'), App\Api\MeHandler, @userOrPartner)Autorizácia
authorized() beží pred handlerom. Vráti false so správou pre 401, alebo uloží používateľa a vráti true.
<?php
declare(strict_types=1);
namespace App;
use Nette\Http\IRequest;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
/**
* Bearer token that maps to a user with roles. The endpoint passes when the user has the required role.
*/
final class RoleTokenAuthorization implements ApiAuthorizationInterface
{
private ?string $errorMessage = null;
/**
* @param array<string, array{name: string, roles: string[]}> $tokens token => user
*/
public function __construct(
private array $tokens,
private IRequest $request,
private CurrentUser $currentUser,
private ?string $requiredRole = null,
) {
}
public function authorized(): bool
{
$header = (string) $this->request->getHeader('Authorization');
if (!preg_match('/^Bearer\s+(\S+)$/i', $header, $match)) {
$this->errorMessage = 'Missing bearer token';
return false;
}
$user = null;
// hash_equals() for every token, so the response time does not tell which one is close
foreach ($this->tokens as $token => $data) {
if (hash_equals((string) $token, $match[1])) {
$user = new ApiUser($data['name'], $data['roles']);
}
}
if ($user === null) {
$this->errorMessage = 'Unknown token';
return false;
}
if ($this->requiredRole !== null && !$user->hasRole($this->requiredRole)) {
$this->errorMessage = "Role {$this->requiredRole} is required";
return false;
}
$this->currentUser->set($user);
return true;
}
public function getErrorMessage(): ?string
{
return $this->errorMessage;
}
}Ktorákoľvek z viacerých
<?php
declare(strict_types=1);
namespace App;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
/**
* Passes when any of the given authorizations passes, for example a bearer token or an API key.
* When all fail, the error message lists why each one failed.
*/
final class AnyOfAuthorization implements ApiAuthorizationInterface
{
/** @var ApiAuthorizationInterface[] */
private array $authorizations;
private ?string $errorMessage = null;
public function __construct(ApiAuthorizationInterface ...$authorizations)
{
$this->authorizations = $authorizations;
}
public function authorized(): bool
{
$messages = [];
foreach ($this->authorizations as $authorization) {
if ($authorization->authorized()) {
return true;
}
$messages[] = $authorization->getErrorMessage();
}
$this->errorMessage = implode('; ', array_filter($messages));
return false;
}
public function getErrorMessage(): ?string
{
return $this->errorMessage;
}
}Odovzdanie používateľa handleru
CurrentUser je zdieľaná služba: autorizácia do nej zapíše, handler z nej číta.
<?php
declare(strict_types=1);
namespace App;
/**
* One shared service: the authorization fills it, the handler reads it.
* ApiPresenter calls authorized() before handle(), so the user is always set when the handler runs.
*/
final class CurrentUser
{
private ?ApiUser $user = null;
public function set(ApiUser $user): void
{
$this->user = $user;
}
public function get(): ?ApiUser
{
return $this->user;
}
}<?php
declare(strict_types=1);
namespace App\Api;
use App\CurrentUser;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
final class MeHandler extends BaseHandler
{
public function __construct(private CurrentUser $currentUser)
{
parent::__construct();
}
public function summary(): string
{
return 'Who is calling';
}
public function handle(array $params): ResponseInterface
{
// set by RoleTokenAuthorization; null when the endpoint uses an API key only
$user = $this->currentUser->get();
return new JsonApiResponse(200, [
'name' => $user?->name ?? 'api-key client',
'roles' => $user?->roles ?? [],
]);
}
}Test
#!/usr/bin/env bash
# End-to-end check: roles from a bearer token, and one endpoint that accepts a bearer token or an API key.
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
serve 8090
request "$BASE/api/v1/me"
expect_status 401 "no token"
expect_body '{"status":"error","message":"Missing bearer token"}' "no token"
ok "no token answers 401 with the message of the authorization"
request -H 'Authorization: Bearer wrong' "$BASE/api/v1/me"
expect_status 401 "unknown token"
expect_body '"Unknown token"' "unknown token"
ok "an unknown token answers 401"
request -H 'Authorization: Bearer bob-token' "$BASE/api/v1/me"
expect_status 200 "bob"
expect_body '{"name":"Bob","roles":["staff"]}' "bob"
ok "the handler reads the user that the authorization found"
request -H 'Authorization: Bearer bob-token' "$BASE/api/v1/report"
expect_status 401 "bob is not admin"
expect_body '"Role admin is required"' "bob is not admin"
ok "a user without the admin role cannot read the report"
request -H 'Authorization: Bearer alice-token' "$BASE/api/v1/report"
expect_status 200 "alice is admin"
expect_body '"orders":42' "alice is admin"
ok "an admin reads the report"
request -H 'X-API-Key: partner-key' "$BASE/api/v1/me/any"
expect_status 200 "api key"
expect_body '"name":"api-key client"' "api key"
ok "the any-of endpoint accepts an API key"
request -H 'Authorization: Bearer alice-token' "$BASE/api/v1/me/any"
expect_body '"name":"Alice"' "bearer on any-of"
ok "the any-of endpoint accepts a bearer token"
request "$BASE/api/v1/me/any"
expect_status 401 "neither"
expect_body 'Missing bearer token; ' "neither"
ok "with neither, the 401 says why both failed"Časté otázky
Kedy beží authorized()?
Po obmedzení požiadaviek a po načítaní parametrov, pred chybami parametrov a pred handlerom. Pozrite si Spracovanie v ApiPresenteri.
Prečo handler neoveruje rolu?
O tom, kto smie endpoint volať, rozhoduje jeho registrácia, takže ten istý handler môže byť raz verejný a inde iba pre administrátorov. Pozrite si Vrstvy.
Prečo hash_equals() v cykle?
Trvá rovnako dlho bez ohľadu na to, aká časť tokenu je správna, takže čas odpovede nepomáha pri hádaní. Pozrite si Autorizácia.
Čo obsahuje telo 401?
{"status":"error","message":...} s textom z getErrorMessage(). AnyOfAuthorization spojí správy všetkých neúspešných kontrol.
Ako ukážem token v OpenAPI a v konzole?
OpenApiHandler rozozná len vstavané triedy (instanceof). Zdeďte jednu z nich, napríklad BearerTokenAuthorization, alebo odovzdajte vlastné securitySchemes v initData handlera; vlastná trieda tak či tak nedostane security pri operáciách. Pozrite si OpenAPI.