PHPackages                             oihana/php-m2m - 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. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. oihana/php-m2m

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

oihana/php-m2m
==============

Lightweight, OIDC-compliant Machine-to-Machine (M2M) HTTP client for APIs protected by JWT — implements the OAuth 2.0 jwt-bearer flow (RFC 7521 + RFC 7523).

1.0.0.74(2w ago)01↓100%MPL-2.0PHPPHP &gt;=7.4

Since May 13Pushed 2w agoCompare

[ Source](https://github.com/BcommeBois/oihana-php-m2m)[ Packagist](https://packagist.org/packages/oihana/php-m2m)[ Docs](https://github.com/BcommeBois/oihana-php-m2m)[ RSS](/packages/oihana-php-m2m/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (3)Versions (4)Used By (0)

Oihana PHP - M2M
================

[](#oihana-php---m2m)

[![Oihana PHP M2M](https://raw.githubusercontent.com/BcommeBois/oihana-php-m2m/main/assets/images/oihana-php-m2m-logo-inline-512x160.png)](https://raw.githubusercontent.com/BcommeBois/oihana-php-m2m/main/assets/images/oihana-php-m2m-logo-inline-512x160.png)

Lightweight, OIDC-compliant **Machine-to-Machine (M2M) HTTP client** for APIs protected by JWT.

Implements the OAuth 2.0 `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` flow ([RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) + [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)) so a service account can sign a short-lived JWT assertion with its own RSA key, exchange it at the identity provider's token endpoint for an access token, and call a protected API with `Authorization: Bearer …`.

Designed for [Zitadel](https://zitadel.com) out of the box, compatible with any RFC 7523-compliant identity provider (Auth0, Keycloak, …) via a configurable token endpoint path.

[![Latest Version](https://camo.githubusercontent.com/17c05adfe3c70d64be1f42fe5647c2d50dee091dc0e37d87d890c17aa0fce277/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f6968616e612f7068702d6d326d2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oihana/php-m2m)[![Total Downloads](https://camo.githubusercontent.com/8229a501f25a16b0a33935054896aed213cb26875f0b190fe56ad9b309af4c64/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f6968616e612f7068702d6d326d2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oihana/php-m2m)[![License](https://camo.githubusercontent.com/9a8e010a10a492fe59a0666a65494f15e38ae6e8fac3f16fc847bbb436b6c765/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6f6968616e612f7068702d6d326d2e7376673f7374796c653d666c61742d737175617265)](LICENSE)

✨ Features
----------

[](#-features)

- **Two-line setup** from a self-sufficient keyfile JSON — no global config, no service container required.
- **Proactive token caching** — one IdP exchange per ~59 minutes (configurable safety margin).
- **Reactive 401 retry** — handles clock drift, premature revocation, and key rotation with one transparent re-exchange.
- **Strongly typed errors** — `KeyfileInvalidException` distinguishes a dead keyfile from a transient network glitch.
- **IdP-agnostic** — defaults to Zitadel's `/oauth/v2/token`, override `$tokenPath` for Auth0, Keycloak, …
- **Subclass-friendly** — `call()`, `doRequest()`, `decodeResponse()` are `protected` extension hooks for instrumentation, correlation IDs, custom envelopes.
- **Zero hidden state** — pass your own Guzzle client to inject middlewares (logging, retry-on-5xx, telemetry).

📦 Installation
--------------

[](#-installation)

> **Requires [PHP 8.4+](https://php.net/releases/)**

Install via [Composer](https://getcomposer.org):

```
composer require oihana/php-m2m
```

🚀 Quick Start
-------------

[](#-quick-start)

### 1. Obtain a keyfile

[](#1-obtain-a-keyfile)

A keyfile is a JSON document handed to you **once** by the API administrator at service creation (or key rotation). It is **never** persisted server-side — store it securely on the calling service's host (file system, secret manager, …).

A keyfile is **auto-sufficient**: it carries both the cryptographic material (the RSA private key + key id) and the connection metadata (issuer URL, API base URL, OAuth2 scope) so a third-party developer can connect without any further configuration.

```
{
    "type":        "serviceaccount",
    "keyId":       "303384786189271040",
    "key":         "-----BEGIN RSA PRIVATE KEY-----\n…\n-----END RSA PRIVATE KEY-----\n",
    "userId":      "303384547813785600",
    "issuer":      "https://my-org.zitadel.cloud",
    "audience":    "303380000000000000",
    "scope":       "openid profile urn:zitadel:iam:org:project:id:303380000000000000:aud",
    "apiBaseUrl":  "https://api.example.com"
}
```

See [documentation/en/keyfile-format.md](documentation/en/keyfile-format.md) for the full field reference.

### 2. Call the API

[](#2-call-the-api)

```
use oihana\m2m\M2MApiClient;

$client = M2MApiClient::fromKeyfile( '/secrets/m2m-keyfile.json' ) ;

$widgets = $client->get   ( '/widgets' ) ;
$created = $client->post  ( '/widgets'      , [ 'name' => 'Foo' ] ) ;
$updated = $client->patch ( '/widgets/42'   , [ 'name' => 'Bar' ] ) ;
$client->delete( '/widgets/42' ) ;
```

That's it. Token acquisition, caching, refresh, and 401 retry are handled internally.

### 3. Override fields when needed

[](#3-override-fields-when-needed)

A typical use case is pointing a staging keyfile at a local API for end-to-end testing:

```
$client = M2MApiClient::fromKeyfile
(
    keyfilePath : '/secrets/staging-keyfile.json' ,
    apiBaseUrl  : 'http://localhost:8000'         // override apiBaseUrl
) ;
```

Same for `$issuer`, `$scope`, `$tokenPath`, and the underlying Guzzle `$http` client.

🌐 Identity provider compatibility
---------------------------------

[](#-identity-provider-compatibility)

The token endpoint path defaults to `/oauth/v2/token` (Zitadel convention). Override it for other providers:

Identity provider`$tokenPath`Zitadel (default)`/oauth/v2/token`Auth0`/oauth/token`Keycloak`/realms/{realm}/protocol/openid-connect/token`Generic RFC 7523whatever your IdP exposes```
$client = new M2MApiClient
(
    keyfile   : $keyfile ,
    tokenPath : '/oauth/token'  // Auth0
) ;
```

🔁 Token lifecycle
-----------------

[](#-token-lifecycle)

- **Proactive refresh** — the access token is cached in memory with its `expires_in` and reused for every call until 60 seconds before its expiration (`REFRESH_SAFETY_MARGIN`). No exchange happens for ~59 minutes on a typical IdP default.
- **Reactive retry on 401** — if the resource API rejects the cached Bearer (clock drift, key rotation, IdP hiccup), the client invalidates its cache, performs **one** fresh exchange, and replays the request once. If the retry still fails with 401, a `KeyfileInvalidException` is thrown.
- **Other failures bubble up** — only HTTP 401 triggers the refresh-and-retry. Network errors, 5xx, 403 (denied) bubble up as-is so they are never masked by a useless re-exchange.

See [documentation/en/token-lifecycle.md](documentation/en/token-lifecycle.md) for the full sequence diagram.

⚠️ Error handling
-----------------

[](#️-error-handling)

```
use oihana\m2m\M2MApiClient;
use oihana\m2m\exceptions\KeyfileInvalidException;
use GuzzleHttp\Exception\GuzzleException;

try
{
    $data = $client->get( '/widgets' ) ;
}
catch( KeyfileInvalidException $e )
{
    // Keyfile is no longer accepted by the IdP or the API.
    // Action : re-download a fresh keyfile from the admin UI.
}
catch( GuzzleException $e )
{
    // Network / non-401 failure (timeout, DNS, 5xx, …). Retry later.
}
```

See [documentation/en/error-handling.md](documentation/en/error-handling.md).

🧩 Extending the client
----------------------

[](#-extending-the-client)

`call()`, `doRequest()`, and `decodeResponse()` are `protected` extension hooks. Subclass `M2MApiClient` to add per-request instrumentation, correlation IDs, tenant headers, or to support non-JSON envelopes — without rewriting the public verb methods.

```
class TracingM2MClient extends M2MApiClient
{
    protected function doRequest( string $method , string $path , ?array $body , string $token ) :\GuzzleHttp\Psr7\Response
    {
        $started = microtime( true ) ;
        $response = parent::doRequest( $method , $path , $body , $token ) ;
        $this->logger->info( "M2M $method $path" , [ 'ms' => ( microtime( true ) - $started ) * 1000 ] ) ;
        return $response ;
    }
}
```

See [documentation/en/advanced/extending-the-client.md](documentation/en/advanced/extending-the-client.md).

📚 Documentation
---------------

[](#-documentation)

Full documentation is available in two languages :

- 🇬🇧 [English](documentation/en/README.md)
- 🇫🇷 [Français](documentation/fr/README.md)

Topics covered:

- Getting started — install + first call in under 2 minutes.
- Keyfile format — full field reference, security considerations.
- Token lifecycle — caching, proactive refresh, reactive retry on 401.
- Error handling — exception catalogue + recommended recovery actions.
- Tips &amp; best practices — typed constants from `oihana/php-enums` + `oihana/php-files` to avoid magic strings.
- Advanced — HTTP client injection, subclassing for instrumentation, non-Zitadel IdPs.

🧪 Running the tests
-------------------

[](#-running-the-tests)

```
composer install
composer test
```

The unit suite covers the constructor contract, the `fromKeyfile` factory, and the override semantics (including the configurable `tokenPath`). The full token-exchange path is integration-level and exercised end-to-end against a real identity provider.

🛠 Generating the API documentation
----------------------------------

[](#-generating-the-api-documentation)

```
composer doc
```

Generates the phpDocumentor API reference into `docs/`.

🪪 License
---------

[](#-license)

This project is licensed under the [Mozilla Public License 2.0 (MPL-2.0)](LICENSE).

👤 Author
--------

[](#-author)

**Marc Alcaraz** ([@BcommeBois](https://github.com/BcommeBois)) — Project Founder, Lead Developer

- 🌐 Website : [ooop.fr](https://www.ooop.fr)
- 📧 Email :

🔗 Related libraries
-------------------

[](#-related-libraries)

- [oihana/php-enums](https://github.com/BcommeBois/oihana-php-enums) — strongly-typed PHP constant enumerations (HTTP, JWT, OAuth, …)
- [oihana/php-schema](https://github.com/BcommeBois/oihana-php-schema) — Schema.org-aligned data structures, including the `Keyfile` schema
- [oihana/php-files](https://github.com/BcommeBois/oihana-php-files) — file system helpers and MIME type enums

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance96

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% 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 ~8 days

Total

2

Last Release

19d ago

PHP version history (2 changes)1.0.0PHP &gt;=8.4

1.0.0.74PHP &gt;=7.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/d234506e51778dd3d1d61b6c2af6f294ff963651b4aa509b595d3d2a77b7d89b?d=identicon)[ooop](/maintainers/ooop)

---

Top Contributors

[![ekameleon](https://avatars.githubusercontent.com/u/749032?v=4)](https://github.com/ekameleon "ekameleon (8 commits)")

---

Tags

phpjwtoauth2api clientoidcphp74zitadelclient-credentialsm2mmachine-to-machinejwt-bearer

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/oihana-php-m2m/health.svg)

```
[![Health](https://phpackages.com/badges/oihana-php-m2m/health.svg)](https://phpackages.com/packages/oihana-php-m2m)
```

###  Alternatives

[ellaisys/aws-cognito

AWS Cognito package that allows Auth and other related features using the AWS SDK for PHP

121242.9k1](/packages/ellaisys-aws-cognito)[simplesamlphp/simplesamlphp-module-oidc

A SimpleSAMLphp module adding support for the OpenID Connect protocol

5017.7k1](/packages/simplesamlphp-simplesamlphp-module-oidc)[jakub-onderka/openid-connect-php

Bare-bones OpenID Connect client

1154.3k](/packages/jakub-onderka-openid-connect-php)

PHPackages © 2026

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