oleksyuk / oauth2-telegram
Telegram OpenID Connect (Log in with Telegram) provider for league/oauth2-client
Requires
- php: ^8.4
- ext-openssl: *
- firebase/php-jwt: ^7.2
- league/oauth2-client: ^2.9
Requires (Dev)
- ext-sodium: *
- friendsofphp/php-cs-fixer: ^3.95.27
- guzzlehttp/guzzle: ^7.9
- phpstan/phpstan: ^2.3.1
- phpstan/phpstan-deprecation-rules: ^2.0.5
- phpunit/phpunit: ^13.4.3
- psr/simple-cache: ^3.0
- rector/rector: ^2.7
- symfony/cache: ^8.0
Suggests
- ext-sodium: Required to verify ID tokens signed with EdDSA (Ed25519).
- psr/simple-cache: Lets the Telegram provider cache Telegram's signing keys (JWKS) through a PSR-16 cache via the `jwksCache` option.
Provides
None
Conflicts
None
Replaces
None
README
oauth2-telegram
Log in with Telegram (OpenID Connect) provider for league/oauth2-client.
Targets the OpenID Connect flow at https://oauth.telegram.org, not the legacy Login Widget that signs the data with the bot token.
Framework-agnostic: works with plain League, knpuniversity/oauth2-client-bundle and anything else built on league/oauth2-client.
Installation
composer require oleksyuk/oauth2-telegram
Setup in Telegram
- Create a bot (or pick an existing one) in @BotFather.
- Open Bot Settings → Login Widget and add every redirect URI of your app to Allowed URLs.
- Note the Client ID (the bot id) and the Client Secret.
- Optionally pick the ID token signing algorithm under Advanced (
RS256by default). If you change it, pass the same value as thealgorithmoption.
Usage
use Oleksyuk\OAuth2\Client\Provider\Telegram; $provider = new Telegram([ 'clientId' => '1234567890', 'clientSecret' => 'your-client-secret', 'redirectUri' => 'https://example.com/auth/telegram/callback', ]); if (!isset($_GET['code'])) { $url = $provider->getAuthorizationUrl(['scope' => ['openid', 'profile']]); // Keep the state (CSRF) and the PKCE verifier for the callback request. $_SESSION['oauth2state'] = $provider->getState(); $_SESSION['oauth2pkce'] = $provider->getPkceCode(); header('Location: '.$url); exit; } if (!isset($_GET['state'], $_SESSION['oauth2state']) || !hash_equals($_SESSION['oauth2state'], $_GET['state'])) { unset($_SESSION['oauth2state']); exit('Invalid state'); } $provider->setPkceCode($_SESSION['oauth2pkce']); $token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]); // The resource owner is the verified ID token: there is no extra HTTP call to a UserInfo endpoint. $user = $provider->getResourceOwner($token); $user->getId(); // `sub`, stable per user: use it as the account identifier $user->getTelegramId(); // numeric Telegram user id (`id`, with the `profile` scope) $user->getUsername(); // `preferred_username`, may be null $user->getName(); $user->getPicture(); $user->getPhoneNumber(); // with the `phone` scope $user->toArray(); // all claims
Every getter except getId() returns null when the claim is missing.
Options
| Option | Default | Description |
|---|---|---|
algorithm |
RS256 |
ID token signing algorithm chosen in @BotFather: RS256, ES256, ES256K or EdDSA (needs ext-sodium). |
pkceMethod |
S256 |
PKCE method; null disables PKCE. |
jwksCache |
null |
A PSR-16 cache (psr/simple-cache) for Telegram's signing keys. Without it the keys are fetched on every login. |
jwksCacheTtl |
3600 |
Cache lifetime in seconds. On a signature failure with cached keys the set is refetched once (key rotation). |
leeway |
60 |
Allowed clock skew in seconds for exp, nbf and iat. |
Scopes
| Scope | Claims |
|---|---|
openid (required) |
sub, iss, aud, iat, exp |
profile |
id, name, preferred_username, picture |
phone |
phone_number (asks the user for consent) |
telegram:bot_access |
none: lets the bot send messages to the user |
The default scope is openid. With EdDSA or ES256K signing Telegram allows only openid.
Login page language
Telegram's docs list no language parameter, but lang on the authorization URL switches the login page language.
It is undocumented and may stop working:
$url = $provider->getAuthorizationUrl(['lang' => 'uk']);
Errors
Every failure throws Oleksyuk\OAuth2\Client\Exception\TelegramIdentityProviderException, a subclass of
League's IdentityProviderException:
- a token endpoint error. Telegram answers these with HTTP 200 and only an
errorfield, e.g.{"error": "invalid_grant"}for an expired, reused or unknown code. The message is theerrorvalue (error_descriptionif Telegram ever sends one, otherwise the HTTP reason phrase), andgetResponseBody()holds the parsed body; - a missing ID token, a wrong
iss/aud/subor an unusable JWKS; - a failed signature or time check. The original
firebase/php-jwtexception (ExpiredException,SignatureInvalidException,BeforeValidException, …) is available viagetPrevious().
Symfony (knpuniversity/oauth2-client-bundle)
// config/packages/knpu_oauth2_client.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Oleksyuk\OAuth2\Client\Provider\Telegram; return App::config([ 'knpu_oauth2_client' => [ 'clients' => [ 'telegram' => [ 'type' => 'generic', 'provider_class' => Telegram::class, 'client_id' => '%env(TELEGRAM_CLIENT_ID)%', 'client_secret' => '%env(TELEGRAM_CLIENT_SECRET)%', 'redirect_route' => 'auth_oauth_callback', 'redirect_params' => ['provider' => 'telegram'], // The bundle doesn't keep the PKCE verifier between redirect and callback for generic clients. 'provider_options' => ['pkceMethod' => null], ], ], ], ]);
Extra authorization parameters go as the second argument of redirect():
return $clientRegistry->getClient('telegram')->redirect(['openid', 'profile'], ['lang' => 'uk']);
To keep PKCE on, store $client->getOAuth2Provider()->getPkceCode() in the session after building the redirect and call
setPkceCode() on the provider before getAccessToken().
Security
Verified on every getResourceOwner() call:
- the ID token signature against Telegram's JWKS (
https://oauth.telegram.org/.well-known/jwks.json); exp,nbfandiat, with 60 seconds of clock skew allowed by default;issishttps://oauth.telegram.org;audcontains your client id;subis present.
CSRF protection is League's state (check it in your callback, as above), and PKCE (S256) is on by default.
nonce is not supported: Telegram's manual OpenID Connect flow doesn't document it.
Development
task lint # PHPStan (max level), PHP CS Fixer, Rector, composer validate task test # PHPUnit task test:coverage # PHPUnit + coverage report in build/ (needs Xdebug or PCOV) task fix # auto-fix CS Fixer / Rector issues
License
MIT, see LICENSE.md.