thecolony/oauth2-colony
OAuth2 Colony is a League OAuth 2.0 client provider for authenticating with The Colony. It integrates into PHP and Laravel apps, handling authorization redirects, token exchange, and user retrieval through a simple, framework-friendly API.
thecolony/oauth2-colony is a framework-agnostic league/oauth2-client provider that speaks OpenID Connect. It is not a Laravel package — there is no service provider, facade, or Socialite driver — so in a Laravel app you bind it yourself and drive it from a controller. That is actually the day-to-day shape: one container binding, two routes (redirect + callback), and a session slot for state/nonce/PKCE verifier.
Install and orient yourself
composer require thecolony/oauth2-colony
Read in this order:
README.md — constructor options, especially the discovery/issuer URL and the PKCE flag.src/ — the provider class (typically TheColony\OAuth2\Client\Provider\Colony) extending League\OAuth2\Client\Provider\AbstractProvider, plus any ResourceOwner/id_token value object.composer.json — confirm the league/oauth2-client version it pins; that determines whether you get getAuthorizationUrl() option passthrough and PKCE support for free.First use case: log a user in and read their profile
use TheColony\OAuth2\Client\Provider\Colony;
$provider = new Colony([
'clientId' => config('services.colony.client_id'),
'clientSecret' => config('services.colony.client_secret'),
'redirectUri' => route('colony.callback'),
'issuer' => config('services.colony.issuer'), // discovery root
]);
That's the whole mental model. Everything else — discovery, JWKS, id_token validation — hangs off that object.
// app/Providers/AppServiceProvider.php
$this->app->singleton(\TheColony\OAuth2\Client\Provider\Colony::class, fn () => new \TheColony\OAuth2\Client\Provider\Colony([
'clientId' => config('services.colony.client_id'),
'clientSecret' => config('services.colony.client_secret'),
'redirectUri' => route('colony.callback'),
'issuer' => config('services.colony.issuer'),
]));
Add the four keys to config/services.php so config:cache works in production.
OIDC gives you three defenses; use all three even though only state is strictly "required" by the spec.
public function redirect(Colony $provider)
{
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$nonce = bin2hex(random_bytes(16));
session([
'colony.state' => $provider->getState(),
'colony.verifier' => $verifier,
'colony.nonce' => $nonce,
]);
return redirect($provider->getAuthorizationUrl([
'scope' => ['openid', 'profile', 'email'],
'code_challenge' => $challenge,
'code_challenge_method'=> 'S256',
'nonce' => $nonce,
]));
}
getAuthorizationUrl(array $options) merges options into the query string, so this works regardless of whether the provider has a first-class PKCE setter. If the README documents a pkce constructor flag instead, prefer that and drop the manual challenge.
public function callback(Request $request, Colony $provider)
{
abort_unless(hash_equals(session()->pull('colony.state'), (string) $request->query('state')), 403);
abort_if($request->query('error'), 403, $request->query('error_description'));
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->query('code'),
'code_verifier' => session()->pull('colony.verifier'),
]);
$idToken = $token->getValues()['id_token'] ?? null;
// Verify signature against JWKS + iss/aud/exp/nonce. Signature name varies —
// check README; typical shape:
$claims = $provider->verifyIdToken($idToken, session()->pull('colony.nonce'));
$user = User::updateOrCreate(
['colony_sub' => $claims['sub']],
['name' => $claims['name'] ?? null, 'email' => $claims['email'] ?? null]
);
Auth::login($user);
return redirect()->intended('/dashboard');
}
Key the local user on sub, not email. Emails change; sub is the durable OIDC subject.
If you only need identity, prefer the verified id_token claims — no extra round trip. Use $provider->getResourceOwner($token) when you want live profile data (fresh name/avatar) or a UserInfo endpoint that returns more than the id_token. Map whichever you pick into the same internal user record.
For an API issuing tokens to a SPA, you still own the session or a short-lived signed cookie for state/verifier/nonce. Don't drop them just because the backend is stateless — losing nonce silently disables replay protection on the id_token.
"thecolony/oauth2-colony": "x.y.z") rather than ^x.y, skim src/ before shipping to production, and be prepared to fork. The dependency itself is MIT and small, so the fork cost is low — but audit it rather than assume a battle-tested provider..well-known/openid-configuration. Do not do that per request. Cache the discovery doc (and the JWKS) in Laravel's cache, keyed by issuer, with a TTL and a stale-while-revalidate fallback so an outage at the IdP doesn't take down login for users who already have a session.kid lookup and refresh on an unknown kid rather than failing the login. A hardcoded/pinned key set is the classic bug six months in.redirect_uri must match byte-for-byte, including trailing slash and http vs https. Behind a proxy, route() may generate http:// — set TrustProxies and URL::forceScheme('https'), otherwise the value you send differs from the one registered on the IdP.state and nonce are single-use. session()->pull() (not get()) so a replayed callback fails. Wrap the check in hash_equals, and never abort without clearing the session values.openid scope is mandatory for an id_token to be returned at all. Requesting profile email alone gives you an access token and no claims, which looks like "verification is broken" but is really a scope bug.exp/iat/nbf validation fails intermittently if the IdP and your server drift. Check the provider's leeway option; if it has none and drift is a problem, fix NTP rather than disabling validation.client_secret alone doesn't protect the authorization code on a shared redirect. Sending both is spec-legal and strictly better.MockHttpClient (HttpMockClient from league/oauth2-client) into the provider, or bind a MockProvider in the container under a test env. Stub the discovery document and JWKS as fixtures so tests never touch the network; then assert your controller stores/consumes state/verifier/nonce correctly.getBaseAuthorizationUrl(), getResourceOwnerDetailsUrl(), or the token URL when you need to work around a non-conforming deployment, and register your subclass in the container instead. That keeps the upstream package upgradable.error/error_description from the callback, the kid on JWKS misses, and claim validation failures — never the raw id_token or code_verifier.How can I help you explore Laravel packages today?