Search by

oleksyuk / oauth2-telegram

maks-oleksyuk

Telegram OpenID Connect (Log in with Telegram) provider for league/oauth2-client

Package info

github.com/maks-oleksyuk/oauth2-telegram

pkg:composer/oleksyuk/oauth2-telegram

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-11-11 07:20 UTC

This package is auto-updated.

Last update: 2026-11-11 07:33:19 UTC


README

oauth2-telegram

CI Coverage PHP Version Latest Version Total Downloads

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

  1. Create a bot (or pick an existing one) in @BotFather.
  2. Open Bot Settings → Login Widget and add every redirect URI of your app to Allowed URLs.
  3. Note the Client ID (the bot id) and the Client Secret.
  4. Optionally pick the ID token signing algorithm under Advanced (RS256 by default). If you change it, pass the same value as the algorithm option.

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 error field, e.g. {"error": "invalid_grant"} for an expired, reused or unknown code. The message is the error value (error_description if Telegram ever sends one, otherwise the HTTP reason phrase), and getResponseBody() holds the parsed body;
  • a missing ID token, a wrong iss/aud/sub or an unusable JWKS;
  • a failed signature or time check. The original firebase/php-jwt exception (ExpiredException, SignatureInvalidException, BeforeValidException, …) is available via getPrevious().

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, nbf and iat, with 60 seconds of clock skew allowed by default;
  • iss is https://oauth.telegram.org;
  • aud contains your client id;
  • sub is 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.