Skip to content

Authentication & authorization ^2.3 ​

Authentication verifies who the user is. Authorization verifies what the user is allowed to do. These are two independent mechanisms — you can use either or both.

Looking for a ready-made solution?

See ProcessWire authentication for a built-in authenticator and login/logout service that uses ProcessWire's session system.

How levels work ​

Both mechanisms can be configured on three levels: API, service, or endpoint. A level applies to all of its children, but the two mechanisms combine levels differently:

AuthenticationAuthorization
Configured withauthenticate()authorize()
When multiple levelsClosest wins (endpoint > service > API)All run (API → services → endpoint)
Child level overridesBy setting its own authenticatorWith skipAuthorization() and its own callback
Child level opts outskipAuthentication()skipAuthorization()
FailureAuthenticationException (401)AuthorizationException (403)

Authentication ​

Creating an authenticator ​

Authenticators extend the abstract Authenticator class. It provides access to the ProcessWire API via $this->wire (like ApiPlugin). The authenticate() method receives an AuthenticateArgs object and should throw AuthenticationException on failure.

php
use PwJsonApi\{AuthenticateArgs, AuthenticationException, Authenticator};

class ExampleAuth extends Authenticator
{
  public function authenticate(AuthenticateArgs $args): void
  {
    if ($this->wire->user->isLoggedin() === false) {
      throw new AuthenticationException();
    }
  }
}

Setting an authenticator ​

Pass an authenticator instance to the authenticate() method of the API, a service, or an endpoint:

php
use PwJsonApi\Api;

$api = new Api();
$api->authenticate(new ExampleAuth());

All services and endpoints under the level inherit the authenticator.

Authenticator is not chained ​

If multiple levels define an authenticator, the closest to the endpoint wins (endpoint > service > API). Only that authenticator runs — the authenticators on the levels above it are skipped. This allows you to use a different authentication method for a part of the API, or to make it public.

Authorization ​

Authorization is configured by passing a callback to the authorize() method. The callback receives an AuthorizeArgs object and returns bool — false results in a 403 response.

php
use PwJsonApi\AuthorizeArgs;

$service->authorize(function (AuthorizeArgs $args) {
  // Modifying requests require the editor role
  if (!$args->request->isSafeMethod) {
    return $args->user->hasRole('editor');
  }

  // Other requests require the member role
  return $args->user->hasRole('member');
});

Authorization is chained ​

Unlike authentication, all authorization callbacks in the chain are executed in order: API → services → endpoint. If any callback returns false, the request is rejected. Setting a callback on a child level does not replace the callbacks of the levels above it — to replace them, the child level must opt out of them first.

php
// API: must have member role
$api->authorize(function (AuthorizeArgs $args) {
  return $args->user->hasRole('member');
});

// Service: must also have editor role
$api->addService(new ContentService(), function ($service) {
  $service->authorize(function (AuthorizeArgs $args) {
    return $args->user->hasRole('editor');
  });
});

// Both callbacks run: first API, then service

Argument objects ​

Authenticators receive an AuthenticateArgs object and authorization callbacks receive an AuthorizeArgs object. Both contain the same properties:

PropertyTypeDescription
$requestRequestThe current request
$userProcessWire\UserThe current ProcessWire user
$eventProcessWire\HookEventProcessWire URL hook event
$endpointEndpointRequested endpoint ^2.4
$serviceServiceRequested service ^2.4
$servicesServiceListList of all parent services ^2.4
$apiApiAPI instance ^2.4

Public and protected endpoints ​

There are two ways to combine public and protected endpoints in the same API. Start from the first one, and move on only if it does not fit your structure.

Protect specific services ​

The simplest approach is to set the authenticator only on the services that need it, instead of the API:

php
// Only MyProtectedService requires authentication
$api->addService(new MyPublicService());
$api->addService(new MyProtectedService(), function ($service) {
  $service->authenticate(new ExampleAuth());
});

Opting out ^2.5 ​

If most of the API is protected, set the rules on the API and opt specific services or endpoints out of them:

  • skipAuthentication() skips the authenticators inherited from parent levels.
  • skipAuthorization() skips the authorization callbacks inherited from parent levels.
php
// This service is public despite the API authenticator
$api->addService(new MyPublicService(), function ($service) {
  $service->skipAuthentication();
});

// In service init(): this endpoint ignores the parent authorization callbacks
$this->addEndpoint('/status')
  ->get(function () {
    return new Response(['status' => 'ok']);
  })
  ->skipAuthorization();

Opting out follows these rules:

  • Only inherited rules are skipped. An authenticator or authorization callback set on the same level or below still applies.
  • The two are independent. Skipping authentication does not skip authorization, and vice versa.
  • The whole subtree is affected. All services and endpoints under the level inherit the opt-out. A child level cannot undo it, but it can set its own authenticator or authorization callback.

For example, with nested services /foo → /foo/bar → /foo/bar/baz:

PathConfigurationCallbacks run for a requestWhy
/fooauthorize()/fooOwn callback
/foo/barskipAuthorization()NoneSkips /foo, has no own callback
/foo/bar/bazauthorize()/foo/bar/baz onlyOwn callback, /foo is skipped by /foo/bar

Combining an opt-out with a rule on the same level replaces the parent level rules:

php
// Only editors, regardless of the parent level callbacks
$api->addService(new EditorService(), function ($service) {
  $service->skipAuthorization()->authorize(function (AuthorizeArgs $args) {
    return $args->user->hasRole('editor');
  });
});

WARNING

Skipping authorization also removes parent level restrictions such as role or IP checks from the whole subtree. Use it only when the endpoints are meant to be accessible without them.

PublicAuth ^2.4 deprecated ​

Deprecated

PublicAuth is deprecated since 2.5 and will be removed in 3.0. Use skipAuthentication() instead. To log or track requests to public endpoints, use request hooks.

Best practices ​

Keep authentication and authorization separate. An authenticator verifies who the user is, for example a logged-in session or an API key. An authorization callback verifies what the user is allowed to do, for example a role. Do not check whether the user is logged in in an authorization callback.

Protect by default, open explicitly. Set the rules on the highest level that fits, and make exceptions with opting out. New services are then protected automatically, and every public service or endpoint is a deliberate choice.

Make exceptions where the service or endpoint is defined. For a single service or endpoint, use an opt-out or an own rule (skipAuthorization()->authorize(...)) when adding the service or defining the endpoint. Do not add an instanceof branch for a single service to a parent level callback. The exception is then visible where the service is defined, and the parent level callback does not grow with every exception.

Consider whether a subtree needs its own rule. skipAuthorization() alone removes all parent level restrictions from the subtree. Often the right choice is to replace them with a rule of its own instead.

Built-in services follow your rules. The ProcessWire authentication service and the CSRF plugin token endpoint never opt out on their own. If the API instance has rules, opt them out explicitly where needed.

Execution order ​

Authentication and authorization run after on-request hooks, but before other request hooks and plugins:

  1. On-request hooks
  2. Authenticate — closest authenticator runs, unless skipped
  3. Authorize — all authorization callbacks in the chain run (API → services → endpoint), except the ones skipped
  4. Before hooks, including hooks registered by plugins
  5. Endpoint handler
  6. After hooks

See Application lifecycle for the full sequence.

Exceptions ​

Both AuthenticationException (401) and AuthorizationException (403) extend ApiException, so they can be caught in error hooks:

php
use PwJsonApi\{AuthenticationException, AuthorizationException};

$api->hookOnError(function ($args) {
  if ($args->exception instanceof AuthenticationException) {
    $args->response->with(['login_url' => '/login']);
  }
});

Released under the MIT License.