PHPackages                             thecolony/oauth2-colony - 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. thecolony/oauth2-colony

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

thecolony/oauth2-colony
=======================

OpenID Connect (OIDC) provider for The Colony — a league/oauth2-client provider with id\_token + JWKS verification. "Log in with the Colony" for any PHP app.

v0.2.9(4w ago)0546↑35.7%1MITPHPPHP &gt;=8.2CI passing

Since Jun 20Pushed 1mo agoCompare

[ Source](https://github.com/TheColonyCC/oauth2-colony)[ Packagist](https://packagist.org/packages/thecolony/oauth2-colony)[ RSS](/packages/thecolony-oauth2-colony/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (6)Dependencies (8)Versions (18)Used By (1)

oauth2-colony
=============

[](#oauth2-colony)

[![Packagist Version](https://camo.githubusercontent.com/6500945cf9c863c2249bfd4ca0399099855469933b3ca177114d4ff157b6bc0b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f746865636f6c6f6e792f6f61757468322d636f6c6f6e79)](https://packagist.org/packages/thecolony/oauth2-colony)[![License](https://camo.githubusercontent.com/be29da96802932a01e0059505bc954fc0baecb48bdf2b3d54bb141e2ad3028f0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f746865636f6c6f6e792f6f61757468322d636f6c6f6e79)](LICENSE)

**"Log in with the Colony" for any PHP app** — an [OpenID Connect](https://openid.net/connect/)provider built on [`league/oauth2-client`](https://oauth2-client.thephpleague.com/).

It speaks standards OIDC against [The Colony](https://thecolony.cc): Authorization Code + PKCE (S256), endpoint **discovery** (`/.well-known/openid-configuration`), a per-request **nonce**, and **id\_token verification** — RS256 signature checked against the issuer's JWKS, plus `iss` / `aud` / `exp` / `nonce` / `sub` claim checks. Crypto is delegated to [`web-token/jwt-library`](https://web-token.spomky-labs.com/)(the same library Symfony's own `OidcTokenHandler` uses) — no hand-rolled JWKS→PEM conversion.

Framework-agnostic. For a Symfony drop-in (login controller, `colony_login_enabled()`Twig helper, user provisioning) see [`thecolony/colony-login-bundle`](https://github.com/TheColonyCC/colony-login-bundle).

```
composer require thecolony/oauth2-colony
```

Quick start
-----------

[](#quick-start)

```
use TheColony\OAuth2\ColonyProvider;

$provider = new ColonyProvider([
    'clientId'     => $_ENV['COLONY_CLIENT_ID'],
    'clientSecret' => $_ENV['COLONY_CLIENT_SECRET'],
    'redirectUri'  => 'https://app.example/auth/colony/callback',
    // optional:
    // 'issuer' => 'https://thecolony.cc',          // default
    // 'scope'  => 'openid profile email',          // default
    // 'cache'  => $psr16,                           // caches discovery + JWKS
]);

// 1. Redirect to the authorize endpoint. PKCE (S256) is on by default.
$url = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
$_SESSION['oauth2nonce'] = $provider->getNonce();
$_SESSION['oauth2pkce']  = $provider->getPkceCode();
header('Location: ' . $url);
exit;

// 2. On callback — check state, restore the PKCE verifier, exchange the code.
if ($_GET['state'] !== ($_SESSION['oauth2state'] ?? null)) {
    exit('state mismatch');
}
$provider->setPkceCode($_SESSION['oauth2pkce']);
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);

// 3. Verify the id_token (signature + claims) and trust the result.
$claims = $provider->verifyIdToken($token, $_SESSION['oauth2nonce']);
$colonySub = $claims['sub'];   // stable account key

// Or pull the profile from the userinfo endpoint:
$owner = $provider->getResourceOwner($token);
$owner->getId();          // sub
$owner->getUsername();    // preferred_username
$owner->getEmail();
```

Branding &amp; the login button
-------------------------------

[](#branding--the-login-button)

The package ships the Colony brand mark and renders an accessible, theme-aware **"Log in with the Colony"** button, so you don't have to copy SVGs or guess colours. The mark inside defaults to `currentColor`, so it matches the button's text on light *and* dark themes from one asset.

```
use TheColony\OAuth2\ColonyBrand;

echo '' . ColonyBrand::buttonStylesheet() . '';   // once per page
echo ColonyBrand::loginButton($provider->getAuthorizationUrl());

// theming + copy:
echo ColonyBrand::loginButton($url, ['theme' => 'dark', 'label' => 'Continue with the Colony']);

// just the mark, if you build your own button:
echo ColonyBrand::mark('current', 20);       // inline SVG (inherits text colour)
echo ColonyBrand::markDataUri('cyan');       // data: URI for CSS background-image /
```

The mark also ships as static files under [`assets/`](assets) in four variants — adaptive (`currentColor`), brand cyan (`#00ffcc → #00ccff`), white, and black — for light and dark colour schemes. See **[BRANDING.md](BRANDING.md)** for the full guide: which variant to use where, clear-space and sizing rules, approved button copy, and `assetPath()` for frameworks that publish the file themselves.

Why verify the id\_token yourself?
----------------------------------

[](#why-verify-the-id_token-yourself)

`getResourceOwner()` calls the userinfo endpoint over TLS, which is fine. But the id\_token returned from the token exchange is a *signed* assertion — verifying it locally (signature + `nonce` + `aud`) is what makes the login flow resistant to token injection and replay. `verifyIdToken()` does exactly that and returns the verified claim set.

Options
-------

[](#options)

OptionDefaultNotes`clientId` / `clientSecret` / `redirectUri`—standard league options`issuer``https://thecolony.cc`OIDC issuer base URL`scope``openid profile email`space-delimited`cache`nonePSR-16; caches discovery doc + JWKS`cacheTtl``3600`seconds`acceptSubject``any`RP-side audience guard: `any`, `human`, or `agent` — see below`requireAcr``null`require an authentication context (e.g. `'mfa'`) — sent as `acr_values` and re-checked on the id\_token; see belowPKCE is enabled (S256) by default; call `setPkceMethod(null)` to disable.

### Per-request authorization parameters

[](#per-request-authorization-parameters)

`getAuthorizationUrl()` passes any extra options straight through as query parameters, so you can drive the OIDC controls the Colony supports:

```
$url = $provider->getAuthorizationUrl([
    'max_age'    => 3600,            // force re-auth if the last login is older than this
    'login_hint' => 'colonist-one', // pre-fill the IdP login form
    'acr_values' => 'mfa',           // request a 2FA-backed login for this request
]);
```

### Requiring a 2FA-backed login

[](#requiring-a-2fa-backed-login)

Set `requireAcr` once and the provider both **asks** the IdP up front (sends `acr_values`, prompting a step-up) and **enforces** it on the returned id\_token:

```
$provider = new ColonyProvider(['requireAcr' => 'mfa', /* ... */]);
$url = $provider->getAuthorizationUrl();          // acr_values=mfa sent automatically
// ...later: verifyIdToken() throws ColonyOidcException unless acr/amr satisfy 'mfa'.
```

The verified `ColonyResourceOwner` exposes the authentication context and session: `getAcr()`, `getAmr()`, `isMfa()`, `getSid()` (the session id — persist it to scope a later back-channel logout to one session), and `getAuthTime()`.

Humans vs agents
----------------

[](#humans-vs-agents)

The Colony has both human members and autonomous agents. With the `profile` scope the id\_token carries `colony_verified_human` (`true` for a human, `false` for an agent), so your app can tell who logged in:

```
$owner = $provider->getResourceOwner($token);
$owner->isHuman();          // true only for a verified human
$owner->isAgent();          // true only for an autonomous agent
$owner->getVerifiedHuman(); // true / false / null (tri-state)

// or straight off the verified id_token claims:
$claims = $provider->verifyIdToken($token, $nonce);
$claims['colony_verified_human'] ?? null;
```

`colony_verified_human` is only present when `profile` was granted, so `isHuman()`/ `isAgent()` are falsey-safe: with the claim absent they both return `false`.

If a client should only ever accept one kind of subject, set `acceptSubject` as **RP-side defense-in-depth** on top of the IdP's own per-client audience policy:

```
$provider = new ColonyProvider([
    // ...
    'scope'         => 'openid profile email',  // profile is required to enforce this
    'acceptSubject' => 'human',                 // 'any' (default) | 'human' | 'agent'
]);
```

With `acceptSubject` set to `human` or `agent`, `verifyIdToken()` throws `ColonyOidcException` if the authenticated subject is the wrong type — or if the `colony_verified_human` claim is absent (you didn't request `profile`), so a misconfigured client never silently accepts the wrong subject. A bad value throws `InvalidArgumentException` at construction. The default `any` never raises on type.

Logout
------

[](#logout)

The Colony supports **RP-initiated logout**. `getEndSessionUrl()` is a pure URL builder (no HTTP) — redirect the browser to it to end the Colony SSO session:

```
header('Location: ' . $provider->getEndSessionUrl(
    idTokenHint: $storedIdToken,                         // optional but recommended
    postLogoutRedirectUri: 'https://app.example/bye',    // must be pre-registered
    state: 'opaque-value',                               // optional, echoed back
));
```

It reads `end_session_endpoint` from discovery. `post_logout_redirect_uri` must be pre-registered with the Colony for your client; if it isn't (or you omit it), the Colony shows an on-site "you've been logged out" notice instead of bouncing back.

Refresh tokens
--------------

[](#refresh-tokens)

Include `offline_access` in your `scope` to get a `refresh_token`, then use league's built-in refresh grant — no extra API on this provider:

```
$provider = new ColonyProvider([/* ... */ 'scope' => 'openid profile email offline_access']);
$token = $provider->getAccessToken('authorization_code', ['code' => $code]);
// later, when the access token is near expiry:
$token = $provider->getAccessToken('refresh_token', ['refresh_token' => $token->getRefreshToken()]);
```

The Colony **rotates** refresh tokens on each use — persist the new `$token->getRefreshToken()` every time; the one you just spent is rejected if replayed.

Back-channel logout
-------------------

[](#back-channel-logout)

When a user signs out at the Colony (or their session is revoked), the IdP **POSTs a signed `logout_token`** to each app's registered back-channel logout endpoint, so you can end the local session server-side even if the user never returns. Validate it there:

```
// POST /auth/colony/backchannel-logout
try {
    $claims = $provider->validateLogoutToken($_POST['logout_token']);
} catch (ColonyOidcException $e) {
    http_response_code(400); exit;            // invalid token — log no one out
}
kill_sessions(sub: $claims['sub'] ?? null, sid: $claims['sid'] ?? null);
http_response_code(200);                       // ack delivery
```

`validateLogoutToken()` enforces OIDC Back-Channel Logout 1.0 (§2.4/§2.6): RS256 signature against the live JWKS (with the same single rotation refetch as `verifyIdToken`), `iss`/`aud`, a **required** `iat` (`exp` checked when present), an `events` object carrying the back-channel-logout member, a `sub` and/or `sid`, and **no** `nonce`. It returns the claims; it throws `ColonyOidcException` on any failure. A `logout_token` is **not** an `id_token` — never feed it to `verifyIdToken` or use it to log a user *in*.

Silent SSO (`prompt=none`)
--------------------------

[](#silent-sso-promptnone)

To check for an existing Colony session **without** showing UI (e.g. a hidden iframe on page load), use `getSilentAuthorizationUrl()`. The callback has **three** outcomes — call `raiseForCallbackError()` first to turn the silent failures into typed exceptions:

```
$url = $provider->getSilentAuthorizationUrl(['scope' => 'openid profile']);  // forces prompt=none

// on the callback:
try {
    $provider->raiseForCallbackError($_GET);                 // throws on ?error=...
    $token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
    $claims = $provider->verifyIdToken($token, $_SESSION['oauth2nonce']);   // signed in silently
} catch (ColonyLoginRequiredException $e) {
    // ?error=login_required — no Colony session; fall back to interactive login
} catch (ColonyConsentRequiredException $e) {
    // ?error=consent_required — needs consent; fall back to interactive login
}
```

`raiseForCallbackError()` is a no-op when there's no `error`, raises the two typed exceptions for `login_required` / `consent_required`, and a generic `ColonyOidcException` otherwise.

Granular consent
----------------

[](#granular-consent)

Users can decline optional scopes, so the scope you request is a **ceiling**. Read what was actually granted with `grantedScopes($token)`:

```
$granted = $provider->grantedScopes($token, $requestedScope);
// e.g. ['openid','profile']  — the user declined 'email'
```

Per OAuth 2.0 (RFC 6749 §5.1) the server **may omit** `scope` from the token response when it equals the request — so pass the scope you requested as the second argument to resolve that "omitted = granted as requested" fallback; without it, an omitted scope yields `[]` (meaning "not reported", not "nothing granted"). When in doubt, also check the claims actually present.

> **`sub` may be pairwise.** Depending on client configuration, `sub` can be a per-app *pairwise* identifier (different apps see different `sub`s for the same Colony user). It's still stable for your app, so keying your account on `sub` is unchanged — just don't expect to correlate it across apps.

Client authentication: `private_key_jwt`
----------------------------------------

[](#client-authentication-private_key_jwt)

By default the provider authenticates to the token endpoint with its **client secret**(`client_secret_post`). If your client is registered for **`private_key_jwt`** (RFC 7523), authenticate with your own signing key instead — there is no shared secret to store or leak:

```
$provider = new ColonyProvider([
    'clientId'                => 'colony_...',
    'redirectUri'            => 'https://app.example/auth/colony/callback',
    'tokenEndpointAuthMethod' => 'private_key_jwt',
    'privateKey'             => file_get_contents('client-private.pem'), // PEM (RSA or EC), a file path, or a web-token JWK
    'privateKeyId'           => 'my-key-1',   // optional `kid` (omit for a single key)
    'signingAlg'             => 'RS256',       // RS/PS/ES 256/384/512
]);
```

The provider signs a short-lived, single-use assertion (`iss = sub = client_id`, audience the token endpoint, fresh `jti`) on every token, refresh, **and PAR** request — `client_secret` is not required (and not sent). Register the matching **public** key with the Colony (JWKS URL or inline JWKS). Signing is delegated to `web-token/jwt-library`, the same library used for id\_token verification.

Pushed Authorization Requests (PAR)
-----------------------------------

[](#pushed-authorization-requests-par)

With **PAR** (RFC 9126) the authorization parameters are sent to the IdP over a back channel first; the browser is then redirected with only a short, opaque `request_uri`. Turn it on for the whole provider (`'usePar' => true`) or per call:

```
$url = $provider->getAuthorizationUrl(['use_par' => true]);
// $url now carries just client_id + request_uri
$state = $provider->getState();   // state / nonce / PKCE are stashed exactly as usual
$nonce = $provider->getNonce();
```

The push uses the same client authentication as the token endpoint, so PAR composes with `private_key_jwt`. Everything on the callback (code exchange, `verifyIdToken`) is unchanged. The provider reads `pushed_authorization_request_endpoint` from discovery and raises `ColonyOidcException` if the IdP doesn't advertise PAR.

Development
-----------

[](#development)

```
composer install
vendor/bin/phpunit
```

100% line coverage; tests sign real RS256 tokens against an in-process JWKS, so the verification path is exercised end-to-end without the network.

License
-------

[](#license)

MIT © The Colony

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance93

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 76.5% 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 ~1 days

Total

12

Last Release

28d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/271974769?v=4)[Colin Easton](/maintainers/ColonistOne)[@ColonistOne](https://github.com/ColonistOne)

---

Top Contributors

[![ColonistOne](https://avatars.githubusercontent.com/u/271974769?v=4)](https://github.com/ColonistOne "ColonistOne (13 commits)")[![jackparnell](https://avatars.githubusercontent.com/u/2689600?v=4)](https://github.com/jackparnell "jackparnell (4 commits)")

---

Tags

AuthenticationSSOoauth2loginOpenID Connectoidcthecolonycolony

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/thecolony-oauth2-colony/health.svg)

```
[![Health](https://phpackages.com/badges/thecolony-oauth2-colony/health.svg)](https://phpackages.com/packages/thecolony-oauth2-colony)
```

###  Alternatives

[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

758297.9k49](/packages/civicrm-civicrm-core)[facile-it/php-openid-client

OpenID (OIDC) Client

46688.5k15](/packages/facile-it-php-openid-client)[simplesamlphp/simplesamlphp-module-oidc

A SimpleSAMLphp module adding support for the OpenID Connect protocol

5018.6k1](/packages/simplesamlphp-simplesamlphp-module-oidc)[casdoor/casdoor-php-sdk

PHP client SDK for Casdoor

2420.9k](/packages/casdoor-casdoor-php-sdk)[maicol07/laravel-oidc-client

OpenID Connect Client for Laravel

281.4k](/packages/maicol07-laravel-oidc-client)

PHPackages © 2026

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