thecolony / colony-login-bundle
Symfony bundle for "Log in with the Colony" — a drop-in OIDC login button (controller, routes, Twig helper, pluggable user provisioning) on top of thecolony/oauth2-colony.
Package info
github.com/TheColonyAI/colony-login-bundle
Type:symfony-bundle
pkg:composer/thecolony/colony-login-bundle
Requires
- php: >=8.2
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/framework-bundle: ^6.4 || ^7.0
- symfony/http-foundation: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- symfony/routing: ^6.4 || ^7.0
- symfony/security-bundle: ^6.4 || ^7.0
- symfony/security-core: ^6.4 || ^7.0
- thecolony/oauth2-colony: ^0.2.4
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
- symfony/cache: ^6.4 || ^7.0
- symfony/twig-bundle: ^6.4 || ^7.0
- twig/twig: ^3.0
Suggests
- symfony/cache: For caching OIDC discovery + JWKS (pass a PSR-6 pool via colony_login.cache)
- symfony/twig-bundle: For the colony_login_button()/colony_login_enabled() Twig helpers that render the login button
This package is auto-updated.
Last update: 2026-07-21 15:01:25 UTC
README
"Log in with the Colony" for Symfony — in three steps.
A thin Symfony bundle over thecolony/oauth2-colony:
it ships the OIDC login controller + routes, a branded colony_login_button()
Twig helper, and a pluggable user-provisioning interface. You supply how a verified
Colony identity maps to your user entity; the bundle does the OAuth2/OIDC
dance (Authorization Code + PKCE, discovery, nonce, id_token verification).
Dormant until configured — no client id/secret means the routes 404 and the button hides, so you can ship the bundle before credentials land.
composer require thecolony/colony-login-bundle
(Pulls in thecolony/oauth2-colony,
the framework-agnostic OIDC provider this bundle wraps.)
1. Implement the provisioner
Map a verified Colony claim set to your application user. Key on sub — it is
stable; username and email are not.
namespace App\Security; use App\Entity\User; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\User\UserInterface; use TheColony\ColonyLoginBundle\Security\ColonyUserProvisionerInterface; final class ColonyUserProvisioner implements ColonyUserProvisionerInterface { public function __construct( private UserRepository $users, private EntityManagerInterface $em, ) {} public function provision(array $claims): UserInterface { $sub = (string) $claims['sub']; $user = $this->users->findOneBy(['colonySub' => $sub]) ?? (new User())->setColonySub($sub); // ... link by verified email / set profile from $claims as you wish ... $this->em->persist($user); $this->em->flush(); return $user; } }
2. Configure the bundle
# config/packages/colony_login.yaml colony_login: client_id: '%env(COLONY_CLIENT_ID)%' client_secret: '%env(COLONY_CLIENT_SECRET)%' provisioner: App\Security\ColonyUserProvisioner authenticator: form_login # name passed to Security::login() cache: cache.app # PSR-6 pool; caches discovery + JWKS default_uri: '%env(default::DEFAULT_URI)%' # canonical origin (optional) # optional — enables POST /auth/colony/backchannel-logout (see below): backchannel_logout_handler: App\Security\ColonyLogoutHandler routes: success: app_dashboard failure: app_login # issuer / scope default to https://thecolony.ai and "openid profile email"
# config/routes/colony_login.yaml colony_login: resource: '@ColonyLoginBundle/src/Controller/' type: attribute
This registers GET /auth/colony (colony_login), GET /auth/colony/callback
(colony_login_callback), GET /auth/colony/silent (colony_login_silent), and
POST /auth/colony/backchannel-logout (colony_login_backchannel). Register the Colony
client's redirect URI as https://<your-app>/auth/colony/callback.
private_key_jwt + PAR (optional)
By default the bundle authenticates to the token endpoint with client_secret
(client_secret_post). If your Colony client is registered for private_key_jwt
(RFC 7523) you can drop the shared secret and authenticate with your own signing key
instead — and optionally turn on PAR (RFC 9126) so the authorization request is pushed
server-side:
colony_login: client_id: '%env(COLONY_CLIENT_ID)%' token_endpoint_auth_method: private_key_jwt private_key: '%env(COLONY_CLIENT_PRIVATE_KEY)%' # PEM string, or a path to a PEM file private_key_id: key-1 # optional `kid` signing_alg: RS256 # RS/PS/ES 256/384/512 (default RS256) use_par: true # optional: RFC 9126 PAR provisioner: App\Security\ColonyUserProvisioner # ... rest as above; client_secret is not needed for private_key_jwt
Register the matching public key with the Colony for this client. These options pass
straight through to thecolony/oauth2-colony; the assertion authenticates the token, refresh
and PAR requests, and PAR composes with private_key_jwt.
Require 2FA (require_acr, optional)
To force a step-up / MFA login, set require_acr (e.g. mfa). The bundle sends
acr_values on the authorization request so the IdP enforces the context up front, then
re-checks the returned id_token's acr/amr:
colony_login: client_id: '%env(COLONY_CLIENT_ID)%' require_acr: mfa # IdP must assert this acr, else the login is rejected provisioner: App\Security\ColonyUserProvisioner # ... rest as above
Passes straight through to thecolony/oauth2-colony (>= 0.2.4). The Python counterpart is
require_acr="mfa" in colony-oidc.
Silent SSO (prompt=none)
GET /auth/colony/silent starts a no-UI authorization (load it in a hidden iframe) to
sign in a user who already has a Colony session. The callback is shared: on
?error=login_required / consent_required it routes to your failure route — i.e. your
interactive login — which is the correct fallback.
Back-channel logout
To end the local session when a user signs out at the Colony (even if they never return
to your app), implement ColonyBackchannelLogoutHandlerInterface and wire it via
backchannel_logout_handler. That turns on POST /auth/colony/backchannel-logout, where
the bundle validates the IdP's signed logout_token and hands you the claims to terminate
sessions for:
final class ColonyLogoutHandler implements ColonyBackchannelLogoutHandlerInterface { public function logout(array $claims): void { // kill local sessions for $claims['sub'] (all of the user's sessions) // and/or the single session $claims['sid']. Needs a session store you can // query by subject/session id (e.g. a DB session handler with a colony_sub // column) — native file sessions can't be looked up this way. } }
The endpoint returns 200 once your handler runs, 400 on an invalid token (nobody is
logged out), and 404 while no handler is configured. It's a server-to-server POST with
no browser session — exempt the path from your firewall (allow anonymous) and from CSRF,
e.g.:
# config/packages/security.yaml — make the back-channel path public access_control: - { path: ^/auth/colony/backchannel-logout$, roles: PUBLIC_ACCESS }
3. Add the button
The bundle ships a branded, accessible "Log in with the Colony" button that
matches the PHP and Python SDKs. Drop it in — it points at the login route and
renders nothing while the integration is unconfigured, so no {% if %} guard
is needed:
{# once, in your <head> (or serve the CSS yourself): #} {{ colony_login_styles() }} {# wherever the button goes: #} {{ colony_login_button() }}
Customise via options — theme (auto follows the visitor's colour scheme,
or light / dark), label, variant, size, class, attributes:
{{ colony_login_button({ theme: 'dark', label: 'Continue with the Colony', class: 'w-full' }) }}
{# point at a different route, or an explicit URL: #}
{{ colony_login_button({ route: 'colony_login_silent' }) }}
{{ colony_login_button({ href: url('colony_login') }) }}
The mark inside defaults to currentColor, so it follows the button's text on
light and dark themes. Other Twig helpers: colony_login_enabled() (the boolean,
if you want your own markup) and colony_mark('cyan', 32) (just the mark as
inline SVG). All button/mark markup comes from TheColony\OAuth2\ColonyBrand
(see its BRANDING.md for variant guidance and approved copy).
Prefer your own button? The old form still works:
{% if colony_login_enabled() %}
<a href="{{ path('colony_login') }}" class="btn">Log in with the Colony</a>
{% endif %}
That's it. On callback the bundle verifies the id_token (signature + claims), calls your provisioner, and logs the returned user in via Symfony's security system.
Why default_uri?
If your app is reachable on more than one host (e.g. www. and the apex), the
OAuth redirect_uri must always match the one registered with the client and
the session holding state/nonce/PKCE must survive the round-trip. Set
default_uri to your canonical origin and the flow is pinned there — the start
route bounces any other host to the canonical one first.
What lives where
| Concern | Package |
|---|---|
| OAuth2/OIDC protocol (discovery, PKCE, id_token + JWKS verify) | thecolony/oauth2-colony |
| Symfony glue (controller, routes, Twig, DI, provisioning seam) | this bundle |
| Your user model + linking policy | your app (the provisioner) |
Development
composer update vendor/bin/phpunit
Unit tests cover the DI wiring and every controller branch except the final
Security::login() success call, which is exercised end-to-end by the reference
integration (Progenly) rather than reconstructed in isolation.
License
MIT © The Colony