PHPackages                             keboola/api-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. keboola/api-bundle

ActiveSymfony-bundle[API Development](/categories/api)

keboola/api-bundle
==================

Keboola API Bundle

6.1.0(3w ago)014.2k↓13.1%MITPHPPHP &gt;=8.2

Since Jun 14Pushed 2w ago13 watchersCompare

[ Source](https://github.com/keboola/api-bundle)[ Packagist](https://packagist.org/packages/keboola/api-bundle)[ RSS](/packages/keboola-api-bundle/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (93)Versions (67)Used By (0)

Keboola API Bundle
==================

[](#keboola-api-bundle)

Symfony bundle providing common functionality for Keboola API applications.

Installation
------------

[](#installation)

Install the package with Composer:

```
composer require keboola/api-bundle
```

Configuration
-------------

[](#configuration)

The bundle expects having `%app_name%` parameter defined in your Symfony configuration.

### Default configuration

[](#default-configuration)

```
keboola_api:
  app_name: '%app_name%'           # application name to use in user agent
  default_service_dns_type: public # default service DNS type to use in ServiceClient, can be 'public' or 'private'
```

Features
--------

[](#features)

### Preconfigured ServiceClient

[](#preconfigured-serviceclient)

The bundle provides a preconfigured `ServiceClient` that can be used to resolve Keboola API URLs. By default, it is configured to use public hostnames, but it can be reconfigured to use internal ones.

```
keboola_api:
  default_service_dns_type: internal
```

#### Using ENV variables

[](#using-env-variables)

If you need to use ENV variable to configure the `default_service_dns_type`, make sure you provide some default value, otherwise the validation will fail with error `The value "" is not allowed for path "keboola_api.default_service_dns_type".`

```
parameters:
  env(API_DNS_TYPE): internal

keboola_api:
  default_service_dns_type: '%env(API_DNS_TYPE)%'
```

### Controller authentication using attributes

[](#controller-authentication-using-attributes)

To use authentication using attributes, configure firewall to use the `keboola.api_bundle.security.attribute_authenticator`:

```
security:
  firewalls:
    attribute:
        lazy: true
        stateless: true
        custom_authenticators:
          - keboola.api_bundle.security.attribute_authenticator
```

Then add any combination of authentication attributes to your controller:

```
use Keboola\ApiBundle\Attribute\StorageApiTokenAuth;
use Keboola\ApiBundle\Security\StorageApiToken\SecurityApiToken;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

#[StorageApiTokenAuth]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token)
  {
    // only requests with valid X-StorageApi-Token will be allowed
  }
}

#[StorageApiTokenAuth(features: ['feat-a', 'feat-b'])]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token)
  {
    // only requests with valid X-StorageApi-Token and project features 'feat-a' AND 'feat-b' is allowed
  }
}

#[StorageApiTokenAuth(features: ['feat-a'])]
#[StorageApiTokenAuth(features: ['feat-b'])]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token)
  {
    // only requests with valid X-StorageApi-Token and any of project features 'feat-a' OR 'feat-b' ise allowed
  }
}

#[ApplicationTokenAuth(scopes: ['something:manage'])]
#[StorageApiTokenAuth]
class Controller {
  public function __invoke(
    string $entityId,
    #[CurrentUser] TokenInterface $token,
  ) {
    // allows request with a valid Manage API token (`X-KBC-ManageApiToken`) or a Kubernetes
    // ServiceAccount JWT (`X-Kubernetes-Authorization: Bearer `) with the 'something:manage'
    // scope, OR any valid X-StorageApi-Token — but with additional checks in controller
    $entity = $this->fetchEntity($entityId);
    if ($token instanceof StorageApiToken && $token->getProjectId() !== $entity->getProjectId()) {
      throw new AccessDeniedHttpException('...');
    }
  }
}
```

`ApplicationTokenAuth` accepts both the Manage API token header (`X-KBC-ManageApiToken`) and the Kubernetes ServiceAccount JWT header (`X-Kubernetes-Authorization`); Connection resolves both to a Manage token, so `scopes`/`isSuperAdmin` checks are identical regardless of which header the request carries.

### Connection programmatic tokens (Storage token exchange)

[](#connection-programmatic-tokens-storage-token-exchange)

`#[StorageApiTokenAuth]` transparently accepts the new Connection programmatic bearer tokens (`kbc_at_*` access tokens and `kbc_pat_*` personal access tokens) in addition to the legacy `X-StorageApi-Token`. Programmatic tokens are exchanged for a legacy Storage token via the Manage API client's `Client::resolveStorageToken()` (which calls Connection's internal resolver endpoint), authenticating with the service's own projected Kubernetes ServiceAccount JWT. The resolver returns the legacy token together with its full token detail (the same payload as Storage's `tokens/verify`), so the exchange is a single HTTP call — no follow-up Storage verification. The result is a normal `StorageApiToken`, so controllers and `#[CurrentUser] StorageApiToken` keep working unchanged — no controller change and no configuration switch.

Callers send `Authorization: Bearer kbc_at_…`/`kbc_pat_…` together with an `X-KBC-ProjectId` header (the new tokens are not project-scoped on their own).

```
#[StorageApiTokenAuth]
class MyController {
  public function __invoke(#[CurrentUser] StorageApiToken $token) {
    // accepts X-StorageApi-Token (legacy) OR Authorization: Bearer kbc_at_/kbc_pat_ (+ X-KBC-ProjectId)
  }
}
```

This requires the service's ServiceAccount to be mapped to the `internal:auth-bridge:resolve-storage-token` scope in Connection's Kubernetes-auth config. See [docs/storage-token-exchange.md](docs/storage-token-exchange.md) for the full design, the resolver contract, error mapping, and infrastructure prerequisites.

To use individual authentication attributes, you need to install appropriate client package:

- to use `StorageApiTokenAuth`, install `keboola/storage-api-client`

`ApplicationTokenAuth` and the Storage token exchange rely on `keboola/kbc-manage-api-php-client`, which the bundle requires directly, so no extra installation is needed.

Note

If you forget to install appropriate client, you will get exception like `Service "Keboola\ApiBundle\Attribute\ApplicationTokenAuth" not found: the container inside "Symfony\Component\DependencyInjection\Argument\ServiceLocator" is a smaller service locator`

Storage API client
------------------

[](#storage-api-client)

When `#[StorageApiTokenAuth]` is enabled, type-hint `Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory` on a controller argument; the bundle injects a factory already bound to the current request and the resolved `StorageApiToken`. It uses the token resolved by the authenticator, so it works for programmatic (`kbc_pat_*` / `kbc_at_*`) tokens too, and shares the same base `ClientOptions` the bundle uses internally to verify the incoming token — no external Storage client factory needs to be registered by the consuming app.

The base options are preconfigured by the bundle: the Connection (Storage API) URL from the `ServiceClient`, the shared logger, and the configured `app_name` as user agent. The run id comes from the request's `X-KBC-RunId` header, then an optional configured generator (see [Customizing the run id](#customizing-the-run-id)), then a generated `run-*` value; branch / backend come from an optional per-call `ClientOptions`.

The client uses the auth type the request was authenticated with: a legacy or exchanged Storage token is sent as `X-StorageApi-Token`, while an OAuth token (`Authorization: Bearer …`) is sent with the bearer scheme. `StorageApiToken::getTokenType()` exposes it (`AuthType::STORAGE_TOKEN` / `AuthType::BEARER`) so a token value carried as a bearer token is not mistaken for a Storage token if you build a client yourself.

```
#[StorageApiTokenAuth]
public function __invoke(StorageClientApiFactory $storage)
{
    $client = $storage->createClientWrapper()->getBasicClient();

    // branch-aware / per-call overrides:
    // $wrapper = $storage->createClientWrapper(new ClientOptions(branchId: $branchId));
}
```

On a controller that may authenticate through another attribute too (e.g. `#[ApplicationTokenAuth]` alongside `#[StorageApiTokenAuth]` — they are OR'd), the request can be authenticated without a Storage token. Make the argument nullable to receive `null` in that case; a required (non-nullable) argument throws when no Storage token is present:

```
#[ApplicationTokenAuth(scopes: ['something:manage'])]
#[StorageApiTokenAuth]
public function __invoke(?StorageClientApiFactory $storage)
{
    $client = $storage?->createClientWrapper()->getBasicClient(); // null when authenticated via Manage token
}
```

### Configuring the base client options

[](#configuring-the-base-client-options)

The base `ClientOptions` (Connection URL, `@logger`, `app_name` user agent) can be tuned with the optional `storage_client_options` node. Values are **merged onto** the bundle-built base — you never have to reproduce the Connection URL resolution.

Set individual options (only YAML-expressible options are supported here):

```
keboola_api:
  storage_client_options:
    backoff_max_tries: 10
    aws_retries: 5
    aws_debug: false
    retry_on_maintenance: true
    use_branch_storage: false
    user_agent: my-service/1.0
    logger: monolog.logger.storage_api   # a service id for a PSR LoggerInterface
```

Or reference a fully-custom `ClientOptions` service — the only way to set options that cannot be expressed in YAML (`jobPollRetryDelay` closure, `BackendConfiguration`):

```
keboola_api:
  storage_client_options: app.storage_client_options   # service id
```

```
// services.php
$services->set('app.storage_client_options', ClientOptions::class)
    ->args([
        '$backoffMaxTries' => 10,
        '$jobPollRetryDelay' => /* Closure */,
    ]);
```

The service's non-null values are merged over the bundle base via `ClientOptions::addValuesFrom()`. The `service` (string) form and the individual options are mutually exclusive.

### Customizing the run id

[](#customizing-the-run-id)

By default the Storage run id is the request's `X-KBC-RunId` header, falling back to a generated `run-*` value. To generate it yourself when the header is absent, register a `Closure(ClientOptions): string` service and point the `storage_client_options.run_id_generator`knob at it:

```
keboola_api:
  storage_client_options:
    run_id_generator: app.storage_run_id_generator   # service id
```

```
// services.php — a Closure isn't directly constructible, so expose it from a factory.
$services->set('app.storage_run_id_generator', Closure::class)
    ->factory([RunIdGeneratorFactory::class, 'create']);   // returns fn(ClientOptions $o): string
```

The closure receives the resolved per-call `ClientOptions` and must return the run id; the `X-KBC-RunId` header still wins when present. This replaces setting a `runIdGenerator` on a custom `ClientOptions` service, which the Storage client wrapper no longer supports.

`run_id_generator` is grouped under `storage_client_options` for cohesion but is passed to the client factory rather than merged into `ClientOptions`, so — unlike the other keys — it may be combined with the `service` form:

```
keboola_api:
  storage_client_options:
    service: app.storage_client_options       # custom ClientOptions (jobPollRetryDelay, …)
    run_id_generator: app.storage_run_id_generator
```

Testing controllers
-------------------

[](#testing-controllers)

`Keboola\ApiBundle\Test\AuthenticatorTestTrait` stubs the authenticators in functional (`WebTestCase`) tests so guarded controllers can be exercised without reaching real Storage/Manage APIs. It provides five helpers:

HelperStubs auth forReturns`setupFakeStorageApiToken(tokenString?, projectId, features, adminId?)``#[StorageApiTokenAuth]` via legacy `X-StorageApi-Token` (resolves to an `AuthType::STORAGE_TOKEN` token)`StorageApiToken``setupFakeOAuthToken(tokenString?, projectId, features, adminId?)``#[StorageApiTokenAuth]` via an OAuth `Authorization: Bearer` token (resolves to an `AuthType::BEARER` token)`StorageApiToken``setupFakeConnectionToken(projectId, features, tokenString?, adminId?)``#[StorageApiTokenAuth]` via a `kbc_at_`/`kbc_pat_` programmatic token (stubs the exchange resolver client)`StorageApiToken``setupFakeManageApiToken(tokenString, scopes, features)``#[ApplicationTokenAuth]` (both the `X-KBC-ManageApiToken` header and the Kubernetes ServiceAccount JWT)`void``bootCleanClient()`— boots a fresh, reboot-disabled `KernelBrowser` on a clean container`KernelBrowser`Important

`setupFakeStorageApiToken()` / `setupFakeOAuthToken()` only stub token *verification* — the resolved `AuthType` is derived by the authenticator from the request headers, exactly as in production. Send the matching scheme on the request under test: `X-StorageApi-Token: …` for `setupFakeStorageApiToken()` and `Authorization: Bearer …` for `setupFakeOAuthToken()`.

### Why `bootCleanClient()`

[](#why-bootcleanclient)

The `setupFake*Token()` helpers replace services in the test container via `getContainer()->set(...)`. That only works while those services are **not yet initialized**: a `#[StorageApiTokenAuth]` request initializes `ManageApiClientFactory` (it backs the programmatic-token exchange resolver), and an initialized service can no longer be replaced. So whenever a request has already run in the test — or you simply want a guaranteed-clean container — call `self::bootCleanClient()` first. It boots a fresh kernel, disables client reboot, and returns the `KernelBrowser` to use for the request.

Important

`bootCleanClient()` **reboots the kernel**, discarding the previous container. Call it before **every** `getContainer()->set(...)` the test relies on — including your own app-specific service mocks, not just the `setupFake*Token()` helpers. Mocks registered *before* `bootCleanClient()` are thrown away by the reboot and will not be seen by the request.

Recommended order: **seed the database → `bootCleanClient()` → register service mocks → `setupFake*Token()` → request.**

### Requirements

[](#requirements)

- The test case must extend Symfony's `WebTestCase` (`bootCleanClient()` uses `bootKernel()` / `getClient()` / the `test.client` service).
- `symfony/browser-kit` must be installed (dev dependency) for the `KernelBrowser` client.

### Example

[](#example)

```
use Keboola\ApiBundle\Test\AuthenticatorTestTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MyActionTest extends WebTestCase
{
    use AuthenticatorTestTrait;

    public function testUnauthorized(): void
    {
        // A request that doesn't fake a token can use the standard client.
        $client = static::createClient();
        $client->request('GET', '/my-endpoint');

        self::assertResponseStatusCodeSame(401);
    }

    public function testAuthorized(): void
    {
        self::setupDatabase([$entity]);          // 1) seed state the controller reads

        $client = self::bootCleanClient();       // 2) clean container BEFORE any ->set()

        $myService = $this->createMock(MyService::class);
        self::getContainer()->set(MyService::class, $myService);   // 3) app service mocks

        $token = $this->setupFakeStorageApiToken( // 4) stub the authenticator
            projectId: '123',
            features: ['my-feature'],
        );

        $client->request('GET', '/my-endpoint', server: [   // 5) authenticated request
            'HTTP_X_STORAGEAPI_TOKEN' => $token->getTokenValue(),
        ]);

        self::assertResponseIsSuccessful();
    }
}
```

Swap `setupFakeStorageApiToken()` for `setupFakeConnectionToken()` (programmatic token) or `setupFakeManageApiToken()` (`#[ApplicationTokenAuth]`) depending on the attribute under test; the clean-client ordering is the same for all three.

License
-------

[](#license)

MIT licensed, see [LICENSE](./LICENSE) file.

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance96

Actively maintained with recent releases

Popularity25

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity72

Established project with proven stability

 Bus Factor1

Top contributor holds 59.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~34 days

Recently: every ~7 days

Total

34

Last Release

25d ago

Major Versions

1.0.1 → 2.0.02024-02-27

2.0.0 → 3.0.02024-03-19

3.11.0 → 4.0.02026-06-01

4.0.0 → 5.0.02026-06-12

5.3.0 → 6.0.02026-07-22

PHP version history (2 changes)0.0.1PHP &gt;=8.1

5.0.0PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/101dbf2551a0709ddab522f97669f13a2c4cc2d0a1e8d009f3af6ba80accb1a9?d=identicon)[Keboola](/maintainers/Keboola)

---

Top Contributors

[![pepamartinec](https://avatars.githubusercontent.com/u/271753?v=4)](https://github.com/pepamartinec "pepamartinec (92 commits)")[![odinuv](https://avatars.githubusercontent.com/u/4319320?v=4)](https://github.com/odinuv "odinuv (21 commits)")[![ErikZigo](https://avatars.githubusercontent.com/u/1726727?v=4)](https://github.com/ErikZigo "ErikZigo (13 commits)")[![zajca](https://avatars.githubusercontent.com/u/180148?v=4)](https://github.com/zajca "zajca (9 commits)")[![ondrajodas](https://avatars.githubusercontent.com/u/12143866?v=4)](https://github.com/ondrajodas "ondrajodas (8 commits)")[![AdamVyborny](https://avatars.githubusercontent.com/u/27994036?v=4)](https://github.com/AdamVyborny "AdamVyborny (5 commits)")[![janvanicek](https://avatars.githubusercontent.com/u/26391551?v=4)](https://github.com/janvanicek "janvanicek (3 commits)")[![MiroCillik](https://avatars.githubusercontent.com/u/1488015?v=4)](https://github.com/MiroCillik "MiroCillik (1 commits)")[![romantmb](https://avatars.githubusercontent.com/u/228957?v=4)](https://github.com/romantmb "romantmb (1 commits)")[![sykora-ji](https://avatars.githubusercontent.com/u/29491790?v=4)](https://github.com/sykora-ji "sykora-ji (1 commits)")

---

Tags

ajda

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/keboola-api-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/keboola-api-bundle/health.svg)](https://phpackages.com/packages/keboola-api-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M777](/packages/sylius-sylius)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M672](/packages/shopware-core)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M535](/packages/pimcore-pimcore)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.7k39.6k](/packages/matomo-matomo)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
