PHP/Laravel пакет для интеграции с VTB ID (OAuth2 авторизация через аккаунт в ВТБ Банке).
🌐 Язык: Русский | English
- Требования
- Установка
- Быстрый старт
- Конфигурация
- Использование
- API Reference
- Примеры использования
- Безопасность
- Тестирование
- PHP 8.0 или выше
- Guzzle HTTP Client 7.x
- Laravel 8.x, 9.x, 10.x, 11.x или 12.x (опционально, для Laravel интеграции)
composer require tigusigalpa/vtb-id-phpphp artisan vendor:publish --tag=vtbid-configДобавьте репозиторий в composer.json:
{
"repositories": [
{
"type": "path",
"url": "./packages/vtb-id-php"
}
],
"require": {
"tigusigalpa/vtb-id-php": "@dev"
}
}Затем установите:
composer require tigusigalpa/vtb-id-php:@devcomposer require tigusigalpa/vtb-id-php
php artisan vendor:publish --tag=vtbid-configДобавьте в .env:
VTBID_CLIENT_ID=ваш_client_id_от_vtb
VTBID_CLIENT_SECRET=ваш_client_secret_от_vtb
VTBID_REDIRECT_URI=https://ваш-сайт.ru/auth/vtb/callbackВ routes/web.php:
// Успешная авторизация
Route::get('/auth/vtb/success', function () {
$user = session('vtbid_user');
// Здесь создайте/обновите пользователя в БД
// и авторизуйте его в системе
dd($user); // Для тестирования
})->name('vtbid.success');
// Ошибка авторизации
Route::get('/auth/vtb/error', function () {
$error = session('error');
return redirect('/login')->with('error', $error);
})->name('vtbid.error');В любом blade-шаблоне:
<a href="{{ route('vtbid.redirect') }}" class="btn btn-primary">
Войти через VTB ID
</a>composer require tigusigalpa/vtb-id-php// config.php
return [
'client_id' => 'your_client_id',
'client_secret' => 'your_client_secret',
'redirect_uri' => 'https://your-domain.com/callback.php',
];<?php
require_once 'vendor/autoload.php';
use Tigusigalpa\VTBID\VTBIDClient;
session_start();
$config = require 'config.php';
$client = new VTBIDClient($config);
$state = $client->generateState();
$_SESSION['vtbid_state'] = $state;
$authUrl = $client->getAuthorizationUrl($state);
header('Location: ' . $authUrl);
exit;<?php
require_once 'vendor/autoload.php';
use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
session_start();
$config = require 'config.php';
$client = new VTBIDClient($config);
$code = $_GET['code'] ?? null;
$state = $_GET['state'] ?? null;
// Проверка state
if (!$code || !isset($_SESSION['vtbid_state']) || $_SESSION['vtbid_state'] !== $state) {
die('Invalid request');
}
unset($_SESSION['vtbid_state']);
try {
// Получение токена
$tokenData = $client->getAccessToken($code);
// Получение данных пользователя
$userData = $client->getUserData($tokenData['access_token']);
// Сохранение в сессию
$_SESSION['vtbid_access_token'] = $tokenData['access_token'];
$_SESSION['vtbid_refresh_token'] = $tokenData['refresh_token'] ?? null;
$_SESSION['vtbid_user'] = $userData->toArray();
// Перенаправление на главную
header('Location: /dashboard.php');
exit;
} catch (VTBIDException $e) {
die('Ошибка авторизации: ' . $e->getMessage());
}<?php
session_start();
if (!isset($_SESSION['vtbid_user'])) {
header('Location: /login.php');
exit;
}
$user = $_SESSION['vtbid_user'];
?>
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<h1>Добро пожаловать, <?= htmlspecialchars($user['name']) ?>!</h1>
<p>Email: <?= htmlspecialchars($user['email']) ?></p>
<p>Телефон: <?= htmlspecialchars($user['mainMobilePhone']) ?></p>
<a href="/logout.php">Выйти</a>
</body>
</html>Теперь пользователи могут авторизоваться через VTB ID.
Добавьте в .env:
VTBID_CLIENT_ID=your_client_id
VTBID_CLIENT_SECRET=your_client_secret
VTBID_REDIRECT_URI=https://your-domain.com/auth/vtb/callback
# Опциональные параметры
VTBID_BASE_URL=https://id.vtb.ru
VTBID_USER_DATA_URL=https://gost-id.vtb.ru
VTBID_SCOPE="openid name surname patronymic gender mainMobilePhone email"
VTBID_RESPONSE_TYPE=code
# Настройки маршрутов
VTBID_ROUTES_ENABLED=true
VTBID_ROUTES_PREFIX=auth/vtb- Зарегистрируйтесь на портале VTB ID для разработчиков
- Создайте новое приложение
- Укажите Redirect URI:
https://your-domain.com/auth/vtb/callback - Выберите необходимые scope
- Получите
client_idиclient_secret
openid - Обязательный, базовая авторизация
name - Имя
surname - Фамилия
patronymic - Отчество
gender - Пол
birthdate - Дата рождения
mainMobilePhone - Основной телефон
email - Email
inn - ИНН
snils - СНИЛС
Пакет автоматически регистрирует следующие маршруты:
GET /auth/vtb/redirect- Перенаправление на страницу авторизации VTB IDGET /auth/vtb/callback- Обработка callback от VTB ID
Для начала авторизации просто перенаправьте пользователя на:
return redirect()->route('vtbid.redirect');use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
// Через Dependency Injection
public function __construct(protected VTBIDClient $vtbid)
{
}
// Или через Facade
use Tigusigalpa\VTBID\Facades\VTBID;
// Получение URL для авторизации
$state = VTBID::generateState();
session(['vtbid_state' => $state]);
$authUrl = VTBID::getAuthorizationUrl($state);
return redirect($authUrl);
// Обработка callback
try {
$tokenData = VTBID::getAccessToken($code);
// $tokenData содержит: access_token, refresh_token, expires_in, token_type
$userData = VTBID::getUserData($tokenData['access_token']);
// Доступ к данным пользователя
echo $userData->name;
echo $userData->surname;
echo $userData->email;
echo $userData->getFullName(); // Фамилия Имя Отчество
} catch (VTBIDException $e) {
Log::error('VTB ID Error: ' . $e->getMessage());
}try {
$refreshToken = session('vtbid_refresh_token');
$newTokenData = VTBID::refreshAccessToken($refreshToken);
session([
'vtbid_access_token' => $newTokenData['access_token'],
'vtbid_refresh_token' => $newTokenData['refresh_token'] ?? null,
]);
} catch (VTBIDException $e) {
// Токен истек или невалиден
}try {
$accessToken = session('vtbid_access_token');
VTBID::revokeToken($accessToken, 'access_token');
session()->forget(['vtbid_access_token', 'vtbid_refresh_token', 'vtbid_user']);
} catch (VTBIDException $e) {
Log::error('Failed to revoke token: ' . $e->getMessage());
}// Проверка авторизации
if (vtbid_authenticated()) {
$user = vtbid_user();
$token = vtbid_access_token();
}Объект VTBIDUser содержит следующие поля:
sub- Уникальный идентификатор пользователяname- Имяsurname- Фамилияpatronymic- Отчествоgender- Пол (male/female)birthdate- Дата рожденияmainMobilePhone- Основной мобильный телефонemail- EmailemailVerified- Email подтвержден (boolean)phoneNumberVerified- Телефон подтвержден (boolean)inn- ИННsnils- СНИЛС
$userData->getFullName(); // Возвращает "Фамилия Имя Отчество"
$userData->toArray(); // Преобразует в массив
$userData->getRawData(); // Возвращает исходные данные от APIphp artisan make:migration add_vtb_id_fields_to_users_tablepublic function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('vtb_id')->nullable()->unique()->after('id');
$table->string('surname')->nullable()->after('name');
$table->string('patronymic')->nullable()->after('surname');
$table->string('phone')->nullable()->after('email');
});
}php artisan migrateprotected $fillable = [
'name',
'surname',
'patronymic',
'email',
'phone',
'vtb_id',
'password',
];use App\Models\User;
use Illuminate\Support\Facades\Auth;
Route::get('/auth/vtb/success', function () {
$vtbUser = session('vtbid_user');
$user = User::updateOrCreate(
['vtb_id' => $vtbUser['sub']],
[
'name' => $vtbUser['name'],
'surname' => $vtbUser['surname'],
'patronymic' => $vtbUser['patronymic'],
'email' => $vtbUser['email'],
'phone' => $vtbUser['mainMobilePhone'],
]
);
Auth::login($user, true);
return redirect('/dashboard');
})->name('vtbid.success');public function __construct(array $config = [])Параметры:
client_id(string) - ID приложения VTB IDclient_secret(string) - Секретный ключ приложенияredirect_uri(string) - URL для callbackbase_url(string, optional) - Базовый URL APIuser_data_url(string, optional) - URL для данных пользователяscope(string, optional) - Запрашиваемые разрешенияresponse_type(string, optional) - Тип ответа
public function getAuthorizationUrl(?string $state = null): stringГенерирует URL для перенаправления пользователя на страницу авторизации VTB ID.
public function generateState(): stringГенерирует случайную строку для параметра state (CSRF защита).
public function getAccessToken(string $code): arrayОбменивает код авторизации на access token.
Возвращает:
[
'access_token' => '...',
'refresh_token' => '...',
'expires_in' => 3600,
'token_type' => 'Bearer'
]public function refreshAccessToken(string $refreshToken): arrayОбновляет access token используя refresh token.
public function getUserData(string $accessToken): VTBIDUserПолучает данные пользователя используя access token.
public function revokeToken(string $token, string $tokenTypeHint = 'access_token'): boolОтзывает (аннулирует) токен.
use Tigusigalpa\VTBID\Facades\VTBID;
$authUrl = VTBID::getAuthorizationUrl();
$state = VTBID::generateState();
$tokenData = VTBID::getAccessToken($code);
$userData = VTBID::getUserData($accessToken);
$newTokenData = VTBID::refreshAccessToken($refreshToken);
VTBID::revokeToken($token);Регистрация в app/Http/Kernel.php:
protected $routeMiddleware = [
'vtbid.auth' => \Tigusigalpa\VTBID\Middleware\VTBIDAuthenticated::class,
];Использование:
Route::middleware(['vtbid.auth'])->group(function () {
Route::get('/profile', function () {
return view('profile', ['user' => vtbid_user()]);
});
});Структура проекта:
/
├── vendor/
├── config.php
├── login.php
├── callback.php
├── dashboard.php
├── logout.php
└── composer.json
logout.php:
<?php
require_once 'vendor/autoload.php';
use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
session_start();
$config = require 'config.php';
$client = new VTBIDClient($config);
// Отзыв токена
if (isset($_SESSION['vtbid_access_token'])) {
try {
$client->revokeToken($_SESSION['vtbid_access_token']);
} catch (VTBIDException $e) {
// Логирование ошибки
error_log('Failed to revoke token: ' . $e->getMessage());
}
}
// Очистка сессии
session_destroy();
header('Location: /login.php');
exit;Обновление токена:
<?php
require_once 'vendor/autoload.php';
use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
session_start();
$config = require 'config.php';
$client = new VTBIDClient($config);
if (isset($_SESSION['vtbid_refresh_token'])) {
try {
$newTokenData = $client->refreshAccessToken($_SESSION['vtbid_refresh_token']);
$_SESSION['vtbid_access_token'] = $newTokenData['access_token'];
$_SESSION['vtbid_refresh_token'] = $newTokenData['refresh_token'] ?? $_SESSION['vtbid_refresh_token'];
echo "Токен обновлен успешно!";
} catch (VTBIDException $e) {
echo "Ошибка обновления токена: " . $e->getMessage();
header('Location: /login.php');
exit;
}
}Интеграция с базой данных:
<?php
// callback.php с сохранением в БД
require_once 'vendor/autoload.php';
use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
session_start();
$config = require 'config.php';
$client = new VTBIDClient($config);
$code = $_GET['code'] ?? null;
$state = $_GET['state'] ?? null;
if (!$code || !isset($_SESSION['vtbid_state']) || $_SESSION['vtbid_state'] !== $state) {
die('Invalid request');
}
unset($_SESSION['vtbid_state']);
try {
$tokenData = $client->getAccessToken($code);
$userData = $client->getUserData($tokenData['access_token']);
// Подключение к БД
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'username', 'password');
// Поиск или создание пользователя
$stmt = $pdo->prepare("
INSERT INTO users (vtb_id, name, surname, patronymic, email, phone, created_at, updated_at)
VALUES (:vtb_id, :name, :surname, :patronymic, :email, :phone, NOW(), NOW())
ON DUPLICATE KEY UPDATE
name = :name,
surname = :surname,
patronymic = :patronymic,
email = :email,
phone = :phone,
updated_at = NOW()
");
$stmt->execute([
'vtb_id' => $userData->sub,
'name' => $userData->name,
'surname' => $userData->surname,
'patronymic' => $userData->patronymic,
'email' => $userData->email,
'phone' => $userData->mainMobilePhone,
]);
$userId = $pdo->lastInsertId() ?: $pdo->query("SELECT id FROM users WHERE vtb_id = '{$userData->sub}'")->fetchColumn();
// Сохранение в сессию
$_SESSION['user_id'] = $userId;
$_SESSION['vtbid_access_token'] = $tokenData['access_token'];
$_SESSION['vtbid_user'] = $userData->toArray();
header('Location: /dashboard.php');
exit;
} catch (VTBIDException $e) {
die('Ошибка авторизации: ' . $e->getMessage());
} catch (PDOException $e) {
die('Ошибка БД: ' . $e->getMessage());
}Отключите встроенные маршруты в .env:
VTBID_ROUTES_ENABLED=falseСоздайте свои маршруты:
use Tigusigalpa\VTBID\VTBIDClient;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
Route::get('/custom/vtb/login', function (VTBIDClient $vtbid) {
$state = $vtbid->generateState();
session(['vtbid_state' => $state]);
return redirect($vtbid->getAuthorizationUrl($state));
});
Route::get('/custom/vtb/callback', function (Request $request, VTBIDClient $vtbid) {
$code = $request->input('code');
$state = $request->input('state');
if (!$code || session('vtbid_state') !== $state) {
return redirect()->route('login')->with('error', 'Invalid request');
}
try {
$tokenData = $vtbid->getAccessToken($code);
$userData = $vtbid->getUserData($tokenData['access_token']);
// Ваша логика
return redirect()->route('dashboard');
} catch (VTBIDException $e) {
return redirect()->route('login')->with('error', $e->getMessage());
}
});Глобальный обработчик в app/Exceptions/Handler.php:
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
public function register()
{
$this->renderable(function (VTBIDException $e, $request) {
\Log::error('VTB ID Exception', [
'message' => $e->getMessage(),
'code' => $e->getCode(),
]);
if ($request->expectsJson()) {
return response()->json([
'error' => 'vtb_id_error',
'message' => $e->getMessage(),
], 400);
}
return redirect()->route('login')
->with('error', 'Ошибка VTB ID: ' . $e->getMessage());
});
}Middleware для автоматического обновления:
namespace App\Http\Middleware;
use Closure;
use Tigusigalpa\VTBID\Facades\VTBID;
use Tigusigalpa\VTBID\Exceptions\VTBIDException;
class RefreshVTBIDToken
{
public function handle(Request $request, Closure $next)
{
if (!session()->has('vtbid_access_token')) {
return redirect()->route('vtbid.redirect');
}
$expiresAt = session('vtbid_token_expires_at');
if ($expiresAt && now()->addMinutes(5)->greaterThan($expiresAt)) {
$refreshToken = session('vtbid_refresh_token');
if ($refreshToken) {
try {
$newTokenData = VTBID::refreshAccessToken($refreshToken);
session([
'vtbid_access_token' => $newTokenData['access_token'],
'vtbid_refresh_token' => $newTokenData['refresh_token'] ?? $refreshToken,
'vtbid_token_expires_at' => now()->addSeconds($newTokenData['expires_in'] ?? 3600),
]);
} catch (VTBIDException $e) {
session()->forget(['vtbid_access_token', 'vtbid_refresh_token']);
return redirect()->route('vtbid.redirect');
}
}
}
return $next($request);
}
}- HTTPS обязателен - VTB ID не работает с HTTP
- Защита client_secret - никогда не коммитьте в Git
- Проверка state - защита от CSRF (уже реализована в пакете)
- Валидация redirect_uri - должен точно совпадать с зарегистрированным
// Проверяйте email_verified
if (!$vtbUser['email_verified']) {
return redirect()->back()
->with('warning', 'Email не подтвержден');
}
// Логируйте попытки авторизации
\Log::info('VTB ID login attempt', [
'vtb_id' => $vtbUser['sub'],
'email' => $vtbUser['email'],
]);APP_URL=https://your-domain.com
VTBID_REDIRECT_URI=https://your-domain.com/auth/vtb/callbackВ config/session.php:
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',php artisan config:cache
php artisan route:cachecomposer testphp artisan tinker>>> config('vtbid.client_id')
=> "your_client_id"
>>> app(Tigusigalpa\VTBID\VTBIDClient::class)
=> Tigusigalpa\VTBID\VTBIDClient {#...}
>>> route('vtbid.redirect')
=> "http://your-domain.test/auth/vtb/redirect"php artisan route:list | grep vtbcomposer dump-autoload
php artisan config:clear
php artisan cache:clearСоздайте маршрут vtbid.success в routes/web.php
Проверьте VTBID_CLIENT_ID и VTBID_CLIENT_SECRET в .env
Убедитесь, что VTBID_REDIRECT_URI точно совпадает с зарегистрированным в VTB ID
- Authorization URL:
https://id.vtb.ru/ - Token URL:
https://id.vtb.ru/oauth2/token - User Data URL:
https://gost-id.vtb.ru/oauth2/me - Revoke Token URL:
https://id.vtb.ru/oauth2/revoke
MIT License. Подробности в LICENSE.md.
Igor Sazonov
- Email: sovletig@gmail.com
- GitHub: @tigusigalpa
Если у вас возникли проблемы или вопросы, создайте issue на GitHub.
Made with ❤️ for Laravel community
