Add H2H authentication service and new example flows

- Implement H2H authentication service for secure client authentication
- Add new examples for authorization code flow and H2H auth service
- Include example for generating client secret
- Update core connector and token request functionality
- Enhance README with new features

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-01-23 20:58:30 +03:00
parent 60c99bca40
commit 2964c391b5
11 changed files with 497 additions and 11 deletions

110
README.md
View File

@@ -14,9 +14,46 @@
composer require apoloz/alfa-bank-sdk
```
## Подготовка сертификатов (mTLS)
Взаимодействие с API Альфа-Банка требует двухстороннего TLS-соединения. Банк не выдает закрытый ключ, вы должны сгенерировать его самостоятельно.
**1. Генерация закрытого ключа и запроса (CSR):**
Используйте OpenSSL для создания ключа и файла запроса. Создайте файл `openssl.cnf`:
```ini
[req]
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
C = RU
L = Moscow
O = Название Вашей Компании
CN = ФИО Ответственного
emailAddress = your_email@company.com
1.2.643.100.4 = 1234567890 (Ваш ИНН)
```
Выполните команды в терминале:
```bash
# Генерация закрытого ключа (храните его в секрете!)
openssl genrsa -out private.key 2048
# Генерация запроса на сертификат
openssl req -new -key private.key -out request.csr -config openssl.cnf -utf8
```
**2. Отправка в Банк:**
Отправьте файл `request.csr` на почту `alfa_api@alfabank.ru`. В ответ банк пришлет публичный сертификат (`client.cer` или `client.crt`).
**3. Использование:**
Поместите полученный сертификат и ваш закрытый ключ в надежное место и укажите пути к ним в `.env` (переменные `ALFA_BAAS_CERT_PATH` и `ALFA_BAAS_KEY_PATH`).
## Конфигурация (Laravel)
Опубликуйте файл конфигурации:
```bash
php artisan vendor:publish --tag=alfa-bank-config
```
@@ -86,16 +123,19 @@ SDK поддерживает две разные модели безопасно
### 1. Подключение к СБП (Pay-in)
**Шаги для получения доступа:**
1. Генерируете пару ключей: `openssl req -x509 -newkey rsa:2048 -keyout client.key -out client.crt -nodes -days 3650`.
2. Отправляете `client.crt` менеджеру банка (acquiring@alfabank.ru).
3. Получаете от банка `alias` (key-name) и `TermNo`.
**Поток данных и подпись:**
* **Transport**: Обязательный **MTLS**. Вы должны передать `client.crt` и `client.key` в конфигурацию Guzzle/Saloon.
* **Payload**: Тело запроса канонизируется по алгоритму **JCS**.
* **Signature**: SHA256withRSA подпись JCS-строки помещается в заголовок `Authorization`.
- **Transport**: Обязательный **MTLS**. Вы должны передать `client.crt` и `client.key` в конфигурацию Guzzle/Saloon.
- **Payload**: Тело запроса канонизируется по алгоритму **JCS**.
- **Signature**: SHA256withRSA подпись JCS-строки помещается в заголовок `Authorization`.
**SDK делает это автоматически**:
```php
// В config/alfa-bank.php
'sbp' => [
@@ -110,15 +150,18 @@ SDK поддерживает две разные модели безопасно
### 2. Подключение к BaaS (Номинальные счета)
**Шаги для получения доступа:**
1. Регистрация в личном кабинете партнера.
2. Получение `client_id` и `client_secret` для OAuth2.
3. Регистрация публичного ключа для проверки подписей выплат.
**Поток данных и подпись:**
* **Auth**: Стандартный OAuth2 Bearer Token.
* **Payload Signature**: Для критических операций (выплаты, идентификация) требуется `digestSignatures`. Это массив CMS-подписей (PKCS7) в формате Base64, которыми подписываются финансовые поля.
- **Auth**: Стандартный OAuth2 Bearer Token.
- **Payload Signature**: Для критических операций (выплаты, идентификация) требуется `digestSignatures`. Это массив CMS-подписей (PKCS7) в формате Base64, которыми подписываются финансовые поля.
**Пример формирования CMS в SDK:**
```php
use Apoloz\AlfaBank\Utils\CmsSigner;
@@ -128,11 +171,59 @@ $signature = CmsSigner::sign($payload, $privateKey, $certificate);
// $signature помещается в массив digestSignatures
```
### 3. Авторизация Authorization Code Flow (H2H)
Для упрощения работы с OAuth2 (H2H) в SDK добавлен специальный helper-сервис `H2HAuthService`.
**1. Инициализация сервиса:**
```php
$auth = AlfaBank::auth(
clientId: '...',
clientSecret: '...',
redirectUri: 'https://...',
certificatePath: '...',
privateKeyPath: '...'
);
```
**2. Получение ссылки на вход:**
```php
$url = $auth->getAuthUrl(['beneficiaries']);
// Перенаправьте пользователя на $url
```
**3. Обмен кода на токен:**
```php
$tokenData = $auth->exchangeCode($code);
// Сохраните $tokenData->refresh_token в БД
```
**4. Обновление токена:**
```php
$tokenData = $auth->refreshToken($savedRefreshToken);
```
Полный пример: [examples/13_h2h_auth_service.php](examples/13_h2h_auth_service.php)
### 4. Генерация Client Secret (Утилита)
Если банк выдал только сертификаты, вы можете сгенерировать `client_secret` самостоятельно:
```php
// См. examples/14_generate_client_secret.php
$baas->send(new RefreshClientSecretRequest($clientId));
```
---
## Архитектура и Потоки данных
### Гибридный поток: СБП -> Номинальный счет
```mermaid
sequenceDiagram
participant Client as Покупатель
@@ -158,6 +249,7 @@ sequenceDiagram
## Примеры использования
### 1. СБП: Прием платежа на номинальный счет
```php
use Apoloz\AlfaBank\Laravel\Facades\Sbp;
use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest;
@@ -175,6 +267,7 @@ echo "QR URL: " . $response->dto()->payload;
```
### 2. Идентификация платежа (BaaS)
Когда платеж пришел на номинальный счет, его нужно привязать к бенефициару:
```php
@@ -195,9 +288,11 @@ AlfaBank::send(new IdentifyTransactionRequest($data));
```
### 3. Работа с номинальными счетами (Бенефициары)
... (existing content) ...
### 4. Самозанятые (Регистрация доходов)
```php
use Apoloz\AlfaBank\Requests\SelfEmployed\RegisterIncomeRequest;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData;
@@ -213,7 +308,9 @@ $response = AlfaBank::send(new RegisterIncomeRequest($data));
```
### 5. Файловый обмен (KYC)
Загрузка сканов документов бенефициаров через pre-signed ссылки:
```php
use Apoloz\AlfaBank\Requests\Files\UploadFileRequest;
@@ -228,7 +325,9 @@ AlfaBank::send(new UploadFileRequest($uploadUrl, $file));
## Продвинутые возможности
### Управление возвратами СБП
Просмотр всей истории попыток возврата по транзакции:
```php
$response = Sbp::send(new GetQrcReversalHistoryRequest($payrrn, $termNo));
foreach ($response->dto()->refunds as $refund) {
@@ -237,6 +336,7 @@ foreach ($response->dto()->refunds as $refund) {
```
### Webhook Handling (Laravel)
```php
// Обработка СБП уведомлений в Listener
public function handle(AlfaWebhookReceived $event) {

View File

@@ -16,10 +16,26 @@ $baas = AlfaBank::baas(
try {
echo "Authenticating...\n";
$baas->login(
// METHOD A: Standard Client Credentials (for simple services)
// $baas->login(
// clientId: 'your-client-id',
// clientSecret: 'your-client-secret'
// );
// METHOD B: Authorization Code Flow (H2H) (for Nominal Accounts)
// See examples/13_h2h_auth_service.php for the full setup
$auth = \Apoloz\AlfaBank\AlfaBank::auth(
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
clientSecret: 'your-client-secret',
redirectUri: 'https://...',
certificatePath: __DIR__ . '/certs/client.cer',
privateKeyPath: __DIR__ . '/certs/private.key',
environment: Environment::SANDBOX
);
// Assuming you have a saved refresh token:
// $auth->refreshToken('saved_refresh_token');
echo "Authentication Successful!\n";
// 2. Create a Beneficiary (Counterparty)

View File

@@ -43,7 +43,14 @@ $payload = new CreateBeneficiaryRequestData(
);
// 2. Login and send request
$baas->login('client-id', 'client-secret');
// For Nominal Accounts, use H2H Auth (Refresh Token)
// See examples/13_h2h_auth_service.php
$auth = AlfaBank::auth(
'client-id', 'client-secret', 'https://callback',
'path/to/cert.pem', 'path/to/key.pem', Environment::SANDBOX
);
$auth->refreshToken('saved-refresh-token'); // Authenticates the connector
$response = $baas->send(new CreateBeneficiaryRequest($payload));
if ($response->successful()) {

View File

@@ -33,7 +33,12 @@ $signCert = file_get_contents('signing.crt');
$signKey = file_get_contents('signing.key');
try {
$baas->login('client_id', 'client_secret');
// Auth: Nominal Accounts require H2H Flow
$auth = AlfaBank::auth(
'client_id', 'client_secret', 'https://callback',
'transport.crt', 'transport.key'
);
$auth->refreshToken('saved_refresh_token_from_db');
// --- ЭТАП 1: Создание Бенефициара (Контрагента) ---
echo "1. Регистрация бенефициара...\n";

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Requests\NominalAccounts\Beneficiaries\GetBeneficiariesRequest;
/**
* Authorization Code Flow (H2H) Example
*
* Сценарий: Обмен полученного в браузере кода на токены и дальнейшая работа через refresh_token.
*/
// 1. Конфигурация
$config = [
'cert' => __DIR__ . '/certs/client.cer',
'key' => __DIR__ . '/certs/private.key',
'client_id' => getenv('ALFA_BAAS_CLIENT_ID'),
'client_secret' => getenv('ALFA_BAAS_CLIENT_SECRET'),
'redirect_uri' => 'https://your-app.com/callback', // Тот же, что указывали при получении кода
'account_number' => '40702810...'
];
// 2. Инициализация коннектора
$baas = AlfaBank::baas(
certificatePath: $config['cert'],
privateKeyPath: $config['key'],
environment: Environment::SANDBOX
);
// ВАЖНО: Мы пока НЕ вызываем login()!
try {
// --- СЦЕНАРИЙ А: Первый вход (у нас есть только код из браузера) ---
// Ссылка для получения кода: https://baas.alfabank.ru/oidc/authorize?response_type=code&client_id=...
$authCode = 'code_from_browser_url'; // <-- Вставьте сюда код
if ($authCode) {
echo "Обмениваем код на токены...\n";
$tokenData = $baas->authWithCode($authCode, $config['redirect_uri']);
echo "Access Token получен!\n";
echo "Refresh Token: " . $tokenData->refresh_token . "\n";
echo "(Сохраните refresh_token в базу данных)\n\n";
}
// --- СЦЕНАРИЙ Б: Повторный вход (у нас есть сохраненный refresh_token) ---
$savedRefreshToken = 'your_saved_refresh_token_from_db';
if ($savedRefreshToken) {
echo "Обновляем сессию по Refresh Token...\n";
$tokenData = $baas->refreshToken($savedRefreshToken);
echo "Сессия обновлена. Новый Access Token действует " . $tokenData->expires_in . " сек.\n";
if ($tokenData->refresh_token) {
echo "Пришел новый Refresh Token (сохраните его): " . $tokenData->refresh_token . "\n";
}
}
// 3. Выполняем защищенный запрос (например, список бенефициаров)
// Коннектор уже авторизован (внутри updateAccessToken/authWithCode вызывается authenticate())
echo "Запрашиваем список...\n";
$response = $baas->send(new GetBeneficiariesRequest(
accountNumber: $config['account_number'],
type: 'FL_RESIDENT'
));
echo "Успешно! Найдено: " . count($response->dto()->beneficiaries) . "\n";
} catch (\Exception $e) {
echo "Ошибка: " . $e->getMessage() . "\n";
if (method_exists($e, 'getResponse')) {
echo "Body: " . $e->getResponse()->body() . "\n";
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
/**
* Пример использования H2HAuthService для удобной авторизации
*/
// 1. Конфигурация
$clientId = getenv('ALFA_BAAS_CLIENT_ID');
$clientSecret = getenv('ALFA_BAAS_CLIENT_SECRET');
$redirectUri = 'https://your-domain.com/callback';
$cert = __DIR__ . '/certs/client.cer';
$key = __DIR__ . '/certs/private.key';
// 2. Инициализация сервиса авторизации
$authService = AlfaBank::auth(
$clientId,
$clientSecret,
$redirectUri,
$cert,
$key,
Environment::SANDBOX
);
// --- СЦЕНАРИЙ 1: Генерация ссылки на вход ---
// Выведите эту ссылку в админке или логах
$link = $authService->getAuthUrl(scopes: ['beneficiaries', 'payment']);
echo "1. Перейдите по ссылке для авторизации:\n$link\n\n";
// --- СЦЕНАРИЙ 2: Обмен кода на токен (в callback-скрипте) ---
// $code приходит в GET-параметре: /callback?code=...
$codeFromUrl = '...код из браузера...';
if ($codeFromUrl !== '...код из браузера...') {
try {
echo "2. Обмен кода на токены...\n";
$tokenData = $authService->exchangeCode($codeFromUrl);
echo "Access Token: " . substr($tokenData->access_token, 0, 20) . "...\n";
echo "Refresh Token: " . $tokenData->refresh_token . "\n";
echo "Expires In: " . $tokenData->expires_in . " sec\n";
// TODO: Сохраните refresh_token в базу данных!
// DB::table('settings')->updateOrInsert(['key' => 'alfa_refresh_token'], ['value' => $tokenData->refresh_token]);
} catch (
Exception $e) {
echo "Ошибка обмена кода: " . $e->getMessage() . "\n";
}
}
// --- СЦЕНАРИЙ 3: Регулярное обновление токена (Cron / Middleware) ---
$savedRefreshToken = '...saved_refresh_token...';
if ($savedRefreshToken !== '...saved_refresh_token...') {
try {
echo "3. Обновление токена...\n";
$newTokenData = $authService->refreshToken($savedRefreshToken);
echo "Токен обновлен!\n";
// Если пришел новый refresh_token — обновляем его
if ($newTokenData->refresh_token) {
// DB::update(...)
echo "Новый Refresh Token получен.\n";
}
} catch (
Exception $e) {
echo "Ошибка обновления токена: " . $e->getMessage() . "\n";
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Requests\Auth\RefreshClientSecretRequest;
use Apoloz\AlfaBank\Enums\Environment;
/**
* UTILITY SCRIPT: Generate a Client Secret using your TLS certificates.
*
* Run this ONCE when setting up the project or if you lose your secret.
*
* Requirements:
* 1. You have the `client_id` (from the bank/portal).
* 2. You have the TLS certificates (.pem/.key) authorized by the bank.
*/
// 1. Configuration
$certPath = __DIR__ . '/certs/transport.crt'; // Your mTLS certificate
$keyPath = __DIR__ . '/certs/transport.key'; // Your mTLS private key
$clientId = 'YOUR_CLIENT_ID_UUID'; // E.g. '0cee0683-85ae-...'
// 2. Initialize Connector (No Login required yet)
$baas = AlfaBank::baas(
certificatePath: $certPath,
privateKeyPath: $keyPath,
environment: Environment::SANDBOX // Change to PRODUCTION for real usage
);
try {
echo "Requesting new Client Secret for Client ID: {$clientId}...\n";
// 3. Send Request
$request = new RefreshClientSecretRequest($clientId);
$response = $baas->send($request);
// 4. Output Result
$data = $response->dto(); // Returns ClientSecretData
echo "\n-------------------------------------------------------\n";
echo "SUCCESS! New Client Secret Generated:\n";
echo "-------------------------------------------------------\n";
echo "Client ID: " . $data->clientId . "\n";
echo "Client Secret: " . $data->clientSecret . "\n";
echo "-------------------------------------------------------\n";
echo "IMPORTANT: Save this to your .env file immediately.\n";
echo "It will not be shown again.\n";
} catch (\Exception $e) {
echo "\nERROR: Could not generate secret.\n";
echo "Message: " . $e->getMessage() . "\n";
if (method_exists($e, 'getResponse')) {
echo "Response: " . $e->getResponse()->body() . "\n";
}
}

View File

@@ -11,10 +11,36 @@ use Apoloz\AlfaBank\Core\AlfaConfig;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Services\OpenSslCmsSigner;
use Apoloz\AlfaBank\Sbp\SbpConnector;
use Apoloz\AlfaBank\Services\H2HAuthService;
use Apoloz\AlfaBank\Requests\Accounts\GetAccountsRequest;
class AlfaBank
{
/**
* Creates a helper service for H2H Authorization (Authorization Code Flow).
*/
public static function auth(
string $clientId,
string $clientSecret,
string $redirectUri,
string $certificatePath,
string $privateKeyPath,
Environment $environment = Environment::SANDBOX
): H2HAuthService {
$config = new \Apoloz\AlfaBank\Core\AlfaConfig(
baseUrl: $environment->value,
certificatePath: $certificatePath,
privateKeyPath: $privateKeyPath,
clientId: $clientId,
clientSecret: $clientSecret
);
$signer = new \Apoloz\AlfaBank\Services\OpenSslCmsSigner($certificatePath, $privateKeyPath);
$connector = new \Apoloz\AlfaBank\Core\AlfaBankConnector($config, $signer);
return new H2HAuthService($connector, $clientId, $redirectUri, $environment);
}
/**
* Performs a health check on the connection.
* Throws an exception if authentication or account listing fails.

View File

@@ -79,6 +79,55 @@ class AlfaBankConnector extends Connector implements HasPagination
return new AlfaOffsetPaginator($this, $request);
}
/**
* Exchange Authorization Code for tokens (H2H Flow).
*/
public function authWithCode(string $code, string $redirectUri): \Apoloz\AlfaBank\Data\Auth\TokenData
{
if (!$this->alfaConfig->clientId || !$this->alfaConfig->clientSecret) {
throw new \RuntimeException("Client ID/Secret not configured.");
}
$request = new GetTokenRequest(
clientId: $this->alfaConfig->clientId,
clientSecret: $this->alfaConfig->clientSecret,
code: $code,
redirectUri: $redirectUri,
grantType: 'authorization_code'
);
$response = $this->send($request);
$data = $response->dto();
$this->authenticate(new \Saloon\Http\Auth\TokenAuthenticator($data->access_token));
return $data;
}
/**
* Refresh access token using refresh token.
*/
public function refreshToken(string $refreshToken): \Apoloz\AlfaBank\Data\Auth\TokenData
{
if (!$this->alfaConfig->clientId || !$this->alfaConfig->clientSecret) {
throw new \RuntimeException("Client ID/Secret not configured.");
}
$request = new GetTokenRequest(
clientId: $this->alfaConfig->clientId,
clientSecret: $this->alfaConfig->clientSecret,
refreshToken: $refreshToken,
grantType: 'refresh_token'
);
$response = $this->send($request);
$data = $response->dto();
$this->authenticate(new \Saloon\Http\Auth\TokenAuthenticator($data->access_token));
return $data;
}
public function login(?string $clientId = null, ?string $clientSecret = null): static
{
// Allow overriding config via method arguments

View File

@@ -42,6 +42,7 @@ class GetTokenRequest extends Request implements HasBody
protected string $clientSecret,
protected ?string $code = null,
protected ?string $redirectUri = null,
protected ?string $refreshToken = null,
protected string $grantType = 'authorization_code'
) {}
// </SECTION>
@@ -64,11 +65,15 @@ class GetTokenRequest extends Request implements HasBody
'client_secret' => $this->clientSecret,
];
if($this->grantType === 'authorization_code') {
if ($this->grantType === 'authorization_code') {
$body['code'] = $this->code;
$body['redirect_uri'] = $this->redirectUri;
}
if ($this->grantType === 'refresh_token') {
$body['refresh_token'] = $this->refreshToken;
}
return $body;
}
// </SECTION>

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Services;
use Apoloz\AlfaBank\Core\AlfaBankConnector;
use Apoloz\AlfaBank\Data\Auth\TokenData;
use Apoloz\AlfaBank\Enums\Environment;
class H2HAuthService
{
public function __construct(
protected AlfaBankConnector $connector,
protected string $clientId,
protected string $redirectUri,
protected Environment $environment = Environment::SANDBOX
) {}
/**
* Step 1: Generate the URL for the user to authorize via Alfa ID.
*
* @param array<string> $scopes List of scopes (e.g. ['beneficiaries', 'payment'])
* @param string|null $state Optional state parameter for CSRF protection
*/
public function getAuthUrl(array $scopes = ['beneficiaries'], ?string $state = null): string
{
$baseUrl = $this->environment === Environment::PRODUCTION
? 'https://id.alfabank.ru/oidc/authorize'
: 'https://id-sandbox.alfabank.ru/oidc/authorize';
$params = [
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'scope' => implode(' ', $scopes),
];
if ($state) {
$params['state'] = $state;
}
return $baseUrl . '?' . http_build_query($params);
}
/**
* Step 2: Exchange the received code for Access & Refresh Tokens.
*/
public function exchangeCode(string $code): TokenData
{
return $this->connector->authWithCode($code, $this->redirectUri);
}
/**
* Step 3: Refresh the token using a stored refresh token.
*/
public function refreshToken(string $refreshToken): TokenData
{
return $this->connector->refreshToken($refreshToken);
}
}