Interface TaskAuthorizationProvider

All Known Implementing Classes:
TestTaskAuthorizationProvider_v0_3

public interface TaskAuthorizationProvider
SPI for per-user task authorization.

Implementers provide a CDI bean (@ApplicationScoped) implementing this interface to control which users can read, write, or create tasks. When no implementation is provided, all operations are denied by default (fail-closed). To allow unauthenticated access without a provider (e.g., for single-user deployments or testing), set the a2a.authorization.required property to false, or call DefaultRequestHandler.builder().authorizationRequired(false) on the builder path.

Providing an implementation

Create an @ApplicationScoped CDI bean that implements this interface. The SDK automatically discovers it and wires it into the request pipeline — no additional configuration is required.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jakarta.enterprise.context.ApplicationScoped;

import org.a2aproject.sdk.server.ServerCallContext;
import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider;
import org.a2aproject.sdk.server.auth.TaskOperation;

@ApplicationScoped
public class MyTaskAuthorizationProvider implements TaskAuthorizationProvider {
    private final ConcurrentMap<String, String> ownershipStore = new ConcurrentHashMap<>();

    @Override
    public boolean checkRead(ServerCallContext context, String taskId, TaskOperation op) {
        return isOwner(context, taskId);
    }

    private boolean isOwner(ServerCallContext context, String taskId) {
        String owner = ownershipStore.get(taskId);
        return owner != null
                && context.getUser() != null
                && owner.equals(context.getUser().getUsername());
    }

    @Override
    public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation op) {
        return isOwner(context, taskId);
    }

    @Override
    public boolean checkCreate(ServerCallContext context, TaskOperation op) {
        return context.getUser() != null && context.getUser().isAuthenticated();
    }

    @Override
    public boolean isTaskRecorded(String taskId) {
        return ownershipStore.containsKey(taskId);
    }

    @Override
    public void recordOwnership(ServerCallContext context, String taskId, TaskOperation op) {
        if (context.getUser() != null) {
            ownershipStore.putIfAbsent(taskId, context.getUser().getUsername());
        }
    }
}

Behavior

When a provider is present, the SDK enforces authorization as follows:

Denied operations throw TaskNotFoundError — the caller cannot distinguish "does not exist" from "not authorized", preventing information leakage.

Thread safety

Implementations must be thread-safe. Methods will be called concurrently from multiple requests.

Ownership recording

recordOwnership(ServerCallContext, String, TaskOperation) is only triggered by onMessageSend and onMessageSendStream — the methods that can create tasks. Other methods (onGetTask, onCancelTask, etc.) do not trigger recording. checkRead(ServerCallContext, String, TaskOperation)/checkWrite(ServerCallContext, String, TaskOperation) may be called for tasks the provider has no ownership data for (e.g., legacy tasks created before the provider was enabled). For production deployments, a fail-closed policy is recommended: deny access when no ownership data exists. An owner == null → allow policy is only appropriate for testing or single-user deployments. If enabling the provider on an existing deployment, consider a migration step to backfill ownership for pre-existing tasks.

Common pitfalls

  • TOCTOU race on ownership recording: The isTaskRecorded(String) → recordOwnership(ServerCallContext, String, TaskOperation) sequence is not atomic. Two concurrent onMessageSend calls for the same new task can both see isTaskRecorded() return false and both call recordOwnership. Implementations must use atomic-insert patterns (e.g., ConcurrentMap.putIfAbsent, INSERT ... ON CONFLICT DO NOTHING) so the first writer wins and the second is a harmless no-op.
See Also:
  • Method Details

    • checkRead

      boolean checkRead(ServerCallContext context, String taskId, TaskOperation operation) throws A2AError
      Check whether the current user is allowed to read the given task.

      For TaskOperation.LIST_TASKS, taskId is an empty-string sentinel representing the whole list scope: the decorator calls this method once before delegation (deny rejects the entire list), and the TaskStore subsequently calls it per task during list() filtering. Providers should treat "" with LIST_TASKS as "may this user list tasks at all" — returning false hides all tasks.

      Parameters:
      context - the server call context containing the authenticated user
      taskId - the task being accessed, or "" for the list scope of LIST_TASKS
      operation - which RequestHandler method triggered the check
      Returns:
      true to allow, false to deny
      Throws:
      A2AError - if the authorization check itself fails
    • checkWrite

      boolean checkWrite(ServerCallContext context, String taskId, TaskOperation operation) throws A2AError
      Check whether the current user is allowed to write to the given task.
      Parameters:
      context - the server call context containing the authenticated user
      taskId - the task being accessed
      operation - which RequestHandler method triggered the check
      Returns:
      true to allow, false to deny
      Throws:
      A2AError - if the authorization check itself fails
    • checkCreate

      boolean checkCreate(ServerCallContext context, TaskOperation operation) throws A2AError
      Check whether the current user is allowed to create a new task.
      Parameters:
      context - the server call context containing the authenticated user
      operation - which RequestHandler method triggered the check
      Returns:
      true to allow, false to deny
      Throws:
      A2AError - if the authorization check itself fails
    • isTaskRecorded

      boolean isTaskRecorded(String taskId) throws A2AError
      Check whether the given task is already known to this provider. Used to avoid redundant recordOwnership(ServerCallContext, String, TaskOperation) calls.
      Parameters:
      taskId - the task to check
      Returns:
      true if ownership has already been recorded for this task
      Throws:
      A2AError - if the check itself fails
    • recordOwnership

      void recordOwnership(ServerCallContext context, String taskId, TaskOperation operation) throws A2AError
      Record that the current user owns the given task. Called after task creation via onMessageSend or onMessageSendStream.

      Must be idempotent. Concurrent requests for the same unrecorded task may both call this method before either completes.

      Parameters:
      context - the server call context containing the authenticated user
      taskId - the newly created task
      operation - which RequestHandler method triggered the recording
      Throws:
      A2AError - if recording fails
    • checkReadAccess

      static boolean checkReadAccess(@Nullable TaskAuthorizationProvider provider, @Nullable ServerCallContext context, String taskId, TaskOperation operation)
      Fail-closed read-access check that handles absent provider and missing call context.

      Returns true (allow) when no provider is configured. Returns false (deny) when a provider is configured but no call context is available. Otherwise delegates to checkRead(ServerCallContext, String, TaskOperation).

      Parameters:
      provider - the authorization provider, or null if authorization is disabled
      context - the server call context, or null if unavailable
      taskId - the task being accessed
      operation - which RequestHandler method triggered the check
      Returns:
      true to allow, false to deny