Update documentation and examples, add new nominal accounts features and authentication methods

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-01-24 17:18:59 +03:00
parent 2964c391b5
commit bb5f91d399
13 changed files with 290 additions and 157 deletions

View File

@@ -1,5 +1,8 @@
# Alfa Bank SDK (Unofficial)
[![Tests](https://github.com/apoloz/alfa-bank-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/apoloz/alfa-bank-sdk/actions)
[![Static Analysis](https://img.shields.io/badge/phpstan-level%205-brightgreen.svg)](https://phpstan.org/)
Профессиональная PHP-библиотека для интеграции с API Альфа-Банка (BaaS и СБП). Построена на базе [Saloon v3](https://docs.saloon.dev) и [Spatie Laravel Data](https://spatie.be/docs/laravel-data).
## Требования
@@ -20,7 +23,6 @@ composer require apoloz/alfa-bank-sdk
**1. Генерация закрытого ключа и запроса (CSR):**
Используйте OpenSSL для создания ключа и файла запроса. Создайте файл `openssl.cnf`:
```ini
[req]
distinguished_name = req_distinguished_name
@@ -35,7 +37,6 @@ emailAddress = your_email@company.com
```
Выполните команды в терминале:
```bash
# Генерация закрытого ключа (храните его в секрете!)
openssl genrsa -out private.key 2048
@@ -53,7 +54,6 @@ openssl req -new -key private.key -out request.csr -config openssl.cnf -utf8
## Конфигурация (Laravel)
Опубликуйте файл конфигурации:
```bash
php artisan vendor:publish --tag=alfa-bank-config
```
@@ -123,19 +123,16 @@ 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' => [
@@ -150,18 +147,15 @@ 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;
@@ -173,57 +167,54 @@ $signature = CmsSigner::sign($payload, $privateKey, $certificate);
### 3. Авторизация Authorization Code Flow (H2H)
Для упрощения работы с OAuth2 (H2H) в SDK добавлен специальный helper-сервис `H2HAuthService`.
**1. Инициализация сервиса:**
SDK использует нативный функционал Saloon для OAuth2.
**1. Инициализация:**
```php
$auth = AlfaBank::auth(
clientId: '...',
clientSecret: '...',
redirectUri: 'https://...',
certificatePath: '...',
privateKeyPath: '...'
$baas = AlfaBank::baas(
clientId: '...', clientSecret: '...', redirectUri: '...',
certificatePath: '...', privateKeyPath: '...'
);
```
**2. Получение ссылки на вход:**
```php
$url = $auth->getAuthUrl(['beneficiaries']);
$url = $baas->getAuthorizationUrl(['beneficiaries']);
// Перенаправьте пользователя на $url
```
**3. Обмен кода на токен:**
```php
$tokenData = $auth->exchangeCode($code);
// Сохраните $tokenData->refresh_token в БД
$authenticator = $baas->getAccessToken($code);
$baas->authenticate($authenticator); // Применить токен
// Сохраните $authenticator->getRefreshToken() в БД
```
**4. Обновление токена:**
```php
$tokenData = $auth->refreshToken($savedRefreshToken);
$newAuth = $baas->refreshAccessToken($savedRefreshToken);
$baas->authenticate($newAuth);
```
Полный пример: [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));
```
> **Важно для продакшена:**
> Запрос нового секрета **инвалидирует** старый. Если у вас запущено несколько экземпляров приложения (или worker-ов), не запрашивайте секрет при каждом старте.
> **Решение:** Получите секрет один раз, сохраните в общий кеш (Redis/DB) с TTL (например, 29 дней) и запрашивайте новый только при истечении срока или отсутствии в кеше.
> Полный пример автоматизации: [examples/16_automated_startup.php](examples/16_automated_startup.php)
---
## Архитектура и Потоки данных
### Гибридный поток: СБП -> Номинальный счет
```mermaid
sequenceDiagram
participant Client as Покупатель
@@ -238,7 +229,7 @@ sequenceDiagram
Note over API, BaaS: Webhook with Signature
API->>BaaS: 4. Webhook (Входящий платеж)
Note over BaaS, API: OAuth2 + CMS Signature
BaaS->>API: 5. IdentifyTransactionRequest(beneficiaryId)
API-->>BaaS: 6. 200 OK (Зачислено)
@@ -249,7 +240,6 @@ sequenceDiagram
## Примеры использования
### 1. СБП: Прием платежа на номинальный счет
```php
use Apoloz\AlfaBank\Laravel\Facades\Sbp;
use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest;
@@ -267,7 +257,6 @@ echo "QR URL: " . $response->dto()->payload;
```
### 2. Идентификация платежа (BaaS)
Когда платеж пришел на номинальный счет, его нужно привязать к бенефициару:
```php
@@ -288,11 +277,9 @@ AlfaBank::send(new IdentifyTransactionRequest($data));
```
### 3. Работа с номинальными счетами (Бенефициары)
... (existing content) ...
### 4. Самозанятые (Регистрация доходов)
```php
use Apoloz\AlfaBank\Requests\SelfEmployed\RegisterIncomeRequest;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData;
@@ -308,9 +295,7 @@ $response = AlfaBank::send(new RegisterIncomeRequest($data));
```
### 5. Файловый обмен (KYC)
Загрузка сканов документов бенефициаров через pre-signed ссылки:
```php
use Apoloz\AlfaBank\Requests\Files\UploadFileRequest;
@@ -325,9 +310,7 @@ AlfaBank::send(new UploadFileRequest($uploadUrl, $file));
## Продвинутые возможности
### Управление возвратами СБП
Просмотр всей истории попыток возврата по транзакции:
```php
$response = Sbp::send(new GetQrcReversalHistoryRequest($payrrn, $termNo));
foreach ($response->dto()->refunds as $refund) {
@@ -336,7 +319,6 @@ foreach ($response->dto()->refunds as $refund) {
```
### Webhook Handling (Laravel)
```php
// Обработка СБП уведомлений в Listener
public function handle(AlfaWebhookReceived $event) {
@@ -346,3 +328,7 @@ public function handle(AlfaWebhookReceived $event) {
}
}
```
## Лицензия
MIT. Автор: Anatoliy Poloz.

View File

@@ -6,24 +6,30 @@ use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
// 1. Initialize
// Ensure you have valid certificates for the Sandbox
// CERTIFICATES:
// - Sandbox: Provided by bank via email or portal (often a .pfx or zip).
// - Production: Generated by YOU (openssl), public part sent to bank for activation.
$baas = AlfaBank::baas(
certificatePath: __DIR__ . '/certs/client.cer',
privateKeyPath: __DIR__ . '/certs/private.key',
environment: Environment::SANDBOX,
caBundlePath: __DIR__ . '/certs/russian_trusted_sub_ca.crt' // Mincifry
caBundlePath: __DIR__ . '/certs/russian_trusted_sub_ca.crt' // Optional: for trusted chain validation
);
try {
echo "Authenticating...\n";
// AUTHENTICATION:
// client_id: From Developer Portal (App Settings)
// client_secret: From Developer Portal (App Settings -> Key Icon) OR generated via SDK (examples/14_generate_client_secret.php)
// METHOD A: Standard Client Credentials (for simple services)
// $baas->login(
// clientId: 'your-client-id',
// clientSecret: 'your-client-secret'
// clientId: 'your-client-id-uuid',
// clientSecret: 'your-client-secret-string'
// );
// METHOD B: Authorization Code Flow (H2H) (for Nominal Accounts)
// METHOD B: Authorization Code Flow (H2H) (REQUIRED for Nominal Accounts)
// See examples/13_h2h_auth_service.php for the full setup
$auth = \Apoloz\AlfaBank\AlfaBank::auth(
clientId: 'your-client-id',

View File

@@ -44,7 +44,11 @@ $payload = new CreateBeneficiaryRequestData(
// 2. Login and send request
// For Nominal Accounts, use H2H Auth (Refresh Token)
// See examples/13_h2h_auth_service.php
// See examples/13_h2h_auth_service.php for setting this up.
// CREDENTIALS SOURCE:
// - client-id: Developer Portal -> App -> Overview
// - client-secret: Developer Portal -> App -> Keys (Click Eye Icon) OR SDK Generator
$auth = AlfaBank::auth(
'client-id', 'client-secret', 'https://callback',
'path/to/cert.pem', 'path/to/key.pem', Environment::SANDBOX

View File

@@ -23,6 +23,8 @@ use Spatie\LaravelData\DataCollection;
*/
// 1. Инициализация SDK
// Сертификаты (Transport Certs):
// Получены от банка (Sandbox) или сгенерированы вами и активированы банком (Production).
$baas = AlfaBank::baas(
certificatePath: 'transport.crt',
privateKeyPath: 'transport.key'
@@ -34,6 +36,8 @@ $signKey = file_get_contents('signing.key');
try {
// Auth: Nominal Accounts require H2H Flow
// client_id: Из Портала разработчика
// client_secret: Из Портала разработчика (глаз) или сгенерирован через SDK (examples/14_generate_client_secret.php)
$auth = AlfaBank::auth(
'client_id', 'client_secret', 'https://callback',
'transport.crt', 'transport.key'

View File

@@ -16,11 +16,24 @@ use Apoloz\AlfaBank\Requests\NominalAccounts\Beneficiaries\GetBeneficiariesReque
// 1. Конфигурация
$config = [
// CERTIFICATES:
// Production: You generated `private.key` and sent CSR to bank. Bank sent back `client.cer`.
// Sandbox: Bank sent you these via email/portal.
'cert' => __DIR__ . '/certs/client.cer',
'key' => __DIR__ . '/certs/private.key',
// APP CREDENTIALS (OAUTH2):
// From: Developer Portal -> Your App -> Settings/Keys
// Note: If you don't have client_secret, generate it using `14_generate_client_secret.php`
'client_id' => getenv('ALFA_BAAS_CLIENT_ID'),
'client_secret' => getenv('ALFA_BAAS_CLIENT_SECRET'),
'redirect_uri' => 'https://your-app.com/callback', // Тот же, что указывали при получении кода
// REDIRECT URI:
// Must match exactly what you entered in Developer Portal for this App.
'redirect_uri' => 'https://your-app.com/callback',
// BUSINESS DATA:
// From your Contract or Internet Bank. Not related to Auth directly.
'account_number' => '40702810...'
];

View File

@@ -12,25 +12,26 @@ use Apoloz\AlfaBank\Enums\Environment;
*/
// 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';
// Все эти данные вы получаете на Портале Разработчика и в Интернет-банке.
$clientId = getenv('ALFA_BAAS_CLIENT_ID'); // ID приложения (Portal)
$clientSecret = getenv('ALFA_BAAS_CLIENT_SECRET'); // Secret Key (Portal OR Generated via SDK)
$redirectUri = 'https://your-domain.com/callback'; // Callback URL (Portal)
$cert = __DIR__ . '/certs/client.cer'; // Public Cert (From Bank)
$key = __DIR__ . '/certs/private.key'; // Private Key (Generated by You)
// 2. Инициализация сервиса авторизации
$authService = AlfaBank::auth(
$clientId,
$clientSecret,
$redirectUri,
$cert,
$key,
Environment::SANDBOX
// 2. Инициализация SDK (сразу с кредами)
$baas = AlfaBank::baas(
certificatePath: $cert,
privateKeyPath: $key,
environment: Environment::SANDBOX,
clientId: $clientId,
clientSecret: $clientSecret,
redirectUri: $redirectUri
);
// --- СЦЕНАРИЙ 1: Генерация ссылки на вход ---
// Выведите эту ссылку в админке или логах
$link = $authService->getAuthUrl(scopes: ['beneficiaries', 'payment']);
// Используем нативный метод Saloon
$link = $baas->getAuthorizationUrl(scopes: ['beneficiaries', 'payment']);
echo "1. Перейдите по ссылке для авторизации:\n$link\n\n";
@@ -41,17 +42,17 @@ $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";
// Saloon делает всё сам:
$authenticator = $baas->getAccessToken($codeFromUrl);
echo "Access Token: " . substr($authenticator->getAccessToken(), 0, 20) . "...\n";
echo "Refresh Token: " . $authenticator->getRefreshToken() . "\n";
echo "Expires At: " . $authenticator->getExpiresAt()->format('Y-m-d H:i:s') . "\n";
// TODO: Сохраните refresh_token в базу данных!
// DB::table('settings')->updateOrInsert(['key' => 'alfa_refresh_token'], ['value' => $tokenData->refresh_token]);
} catch (
Exception $e) {
} catch (\Exception $e) {
echo "Ошибка обмена кода: " . $e->getMessage() . "\n";
}
}
@@ -62,17 +63,21 @@ $savedRefreshToken = '...saved_refresh_token...';
if ($savedRefreshToken !== '...saved_refresh_token...') {
try {
echo "3. Обновление токена...\n";
$newTokenData = $authService->refreshToken($savedRefreshToken);
// Обновляем токен
$newAuthenticator = $baas->refreshAccessToken($savedRefreshToken);
// Автоматически применяем к коннектору (для текущего скрипта)
$baas->authenticate($newAuthenticator);
echo "Токен обновлен!\n";
// Если пришел новый refresh_token — обновляем его
if ($newTokenData->refresh_token) {
if ($newAuthenticator->getRefreshToken()) {
// DB::update(...)
echo "Новый Refresh Token получен.\n";
}
} catch (
Exception $e) {
} catch (\Exception $e) {
echo "Ошибка обновления токена: " . $e->getMessage() . "\n";
}
}

View File

@@ -14,8 +14,10 @@ use Apoloz\AlfaBank\Enums\Environment;
* 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).
* 1. You have the `client_id` (Found in Developer Portal -> App Settings URL or Overview).
* 2. You have the TLS certificates (.pem/.key) authorized by the bank.
* - Prod: You sent CSR to bank, they enabled integration and sent .cer back.
* - Sandbox: You downloaded them from the email/portal.
*/
// 1. Configuration

View File

@@ -0,0 +1,70 @@
<?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;
/**
* FINAL GOAL EXAMPLE: Get List of Nominal Account Beneficiaries
*
* This script assumes you have already:
* 1. Generated/Received Certificates.
* 2. Obtained Client ID & Secret (via Portal or Script 14).
* 3. Passed H2H Auth via Browser and obtained a Refresh Token (via Script 13).
*/
// --- CONFIGURATION (From .env) ---
$config = [
'client_id' => getenv('ALFA_BAAS_CLIENT_ID'),
'client_secret' => getenv('ALFA_BAAS_CLIENT_SECRET'),
'redirect_uri' => 'https://your-app.com/callback',
'cert_path' => __DIR__ . '/certs/client.cer',
'key_path' => __DIR__ . '/certs/private.key',
// BUSINESS DATA
'account_num' => '40702810...', // Nominal Account Number
'refresh_token' => 'saved_refresh_token_db' // Your stored token
];
try {
// 1. Initialize Connector with Auth Config
$baas = AlfaBank::baas(
certificatePath: $config['cert_path'],
privateKeyPath: $config['key_path'],
environment: Environment::SANDBOX,
clientId: $config['client_id'],
clientSecret: $config['client_secret'],
redirectUri: $config['redirect_uri']
);
// 2. Refresh Token (Get valid Access Token for this session)
echo "Refreshing session...\n";
$authenticator = $baas->refreshAccessToken($config['refresh_token']);
// Apply authentication to the connector
$baas->authenticate($authenticator);
// 3. Perform Business Request
echo "Requesting beneficiaries list...\n";
$request = new GetBeneficiariesRequest(
accountNumber: $config['account_num'],
type: 'FL_RESIDENT'
);
$response = $baas->send($request);
// 4. Handle Result
$list = $response->dto();
echo "Found " . count($list->beneficiaries) . " beneficiaries.\n";
foreach ($list->beneficiaries as $b) {
echo "- {$b->name} (ID: {$b->beneficiaryId})\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Requests\Auth\RefreshClientSecretRequest;
use Apoloz\AlfaBank\Requests\NominalAccounts\Beneficiaries\GetBeneficiariesRequest;
use Apoloz\AlfaBank\Enums\Environment;
/**
* FULLY AUTOMATED WORKFLOW
*
* Scenario: You don't want to store `client_secret` persistently.
* Instead, you use your Certificates to fetch a fresh secret on startup (or when needed),
* and then immediately use it to refresh your OAuth tokens.
*
* CAUTION: Requesting a new secret invalidates the old one.
* If you have multiple app instances, implement a shared cache (Redis/DB) for the secret!
*/
// --- CONFIGURATION ---
// You only need immutable identifiers and certificates.
$clientId = getenv('ALFA_BAAS_CLIENT_ID');
$redirectUri = 'https://your-app.com/callback';
$certPath = __DIR__ . '/certs/client.cer';
$keyPath = __DIR__ . '/certs/private.key';
$accNum = '40702810...';
// This assumes you have already passed the browser flow once and saved the refresh token.
$savedRefreshToken = 'your_saved_refresh_token_from_db';
// 1. Initialize Bare Connector (mTLS only)
$baas = AlfaBank::baas($certPath, $keyPath, Environment::SANDBOX);
try {
// 2. FETCH CLIENT SECRET DYNAMICALLY
// We prove our identity via mTLS certificates.
echo "1. Requesting dynamic client_secret via API...\n";
// In a real app: CHECK CACHE FIRST!
// $cachedSecret = Cache::get('alfa_client_secret');
// if ($cachedSecret) { $tempSecret = $cachedSecret; } else { ... }
$secretResponse = $baas->send(new RefreshClientSecretRequest($clientId));
$tempSecret = $secretResponse->dto()->clientSecret;
echo " Secret acquired: " . substr($tempSecret, 0, 5) . "...\n";
// 3. AUTHENTICATE OAUTH2
// Now we use the fresh secret to get an access token.
echo "2. Refreshing OAuth2 session...\n";
// Re-initialize connector with the new secret
$baasReady = AlfaBank::baas(
certificatePath: $certPath,
privateKeyPath: $keyPath,
environment: Environment::SANDBOX,
clientId: $clientId,
clientSecret: $tempSecret,
redirectUri: $redirectUri
);
// Refresh token using native method
$authenticator = $baasReady->refreshAccessToken($savedRefreshToken);
$baasReady->authenticate($authenticator);
echo " Session active. Access Token expires at " . $authenticator->getExpiresAt()->format('H:i:s') . "\n";
// Update DB if refresh token rotated
if ($authenticator->getRefreshToken()) {
// saveNewRefreshToken($authenticator->getRefreshToken());
echo " New Refresh Token saved.\n";
}
// 4. BUSINESS LOGIC
echo "3. Fetching beneficiaries...\n";
$response = $baasReady->send(new GetBeneficiariesRequest($accNum, 'FL_RESIDENT'));
echo " Success! Found " . count($response->dto()->beneficiaries) . " records.\n";
} catch (Exception $e) {
echo "CRITICAL ERROR: " . $e->getMessage() . "\n";
if (method_exists($e, 'getResponse')) {
echo "Body: " . $e->getResponse()->body() . "\n";
}
}

View File

@@ -11,36 +11,10 @@ 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.
@@ -64,6 +38,9 @@ class AlfaBank
string $privateKeyPath,
Environment $environment = Environment::SANDBOX,
?string $caBundlePath = null,
?string $clientId = null,
?string $clientSecret = null,
?string $redirectUri = null,
?string $signingCertificatePath = null,
?string $signingPrivateKeyPath = null
): AlfaBankConnector {
@@ -72,6 +49,9 @@ class AlfaBank
certificatePath: $certificatePath,
privateKeyPath: $privateKeyPath,
caBundlePath: $caBundlePath,
clientId: $clientId,
clientSecret: $clientSecret,
redirectUri: $redirectUri,
signingCertificatePath: $signingCertificatePath,
signingPrivateKeyPath: $signingPrivateKeyPath
);

View File

@@ -9,8 +9,9 @@ namespace Apoloz\AlfaBank\Core;
use Apoloz\AlfaBank\Contracts\SignerInterface;
use Apoloz\AlfaBank\Http\Middleware\RequestIdMiddleware;
use Apoloz\AlfaBank\Http\Pagination\AlfaOffsetPaginator;
use Apoloz\AlfaBank\Requests\Auth\GetTokenRequest;
use Apoloz\AlfaBank\Requests\Auth\GetTokenRequest; // Will be removed
use Apoloz\AlfaBank\Traits\HasAlfaErrorHandling;
use Saloon\Helpers\OAuth2\OAuthConfig;
use Saloon\Http\Connector;
use Saloon\Http\Request;
use Saloon\PaginationPlugin\Contracts\HasPagination;
@@ -18,13 +19,17 @@ use Saloon\PaginationPlugin\OffsetPaginator;
use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Plugins\HasTimeout;
use Saloon\Contracts\OAuth2\OAuth2Api;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;
use Apoloz\AlfaBank\Enums\Environment;
class AlfaBankConnector extends Connector implements HasPagination
class AlfaBankConnector extends Connector implements HasPagination, OAuth2Api
{
use HasTimeout;
use HasTries;
use AlwaysThrowOnErrors;
use HasAlfaErrorHandling;
use AuthorizationCodeGrant;
public function __construct(
protected AlfaConfig $alfaConfig,
@@ -39,6 +44,20 @@ class AlfaBankConnector extends Connector implements HasPagination
return $this->alfaConfig->baseUrl;
}
protected function resolveOauth2Config(): OAuthConfig
{
$idUrl = $this->alfaConfig->baseUrl === Environment::PRODUCTION->value
? Environment::ID_PRODUCTION->value
: Environment::ID_SANDBOX->value;
return OAuthConfig::make()
->setClientId($this->alfaConfig->clientId ?? '')
->setClientSecret($this->alfaConfig->clientSecret ?? '')
->setRedirectUri($this->alfaConfig->redirectUri ?? '')
->setAuthorizeEndpoint($idUrl . '/oidc/authorize')
->setTokenEndpoint('/oidc/token');
}
protected function defaultConfig(): array
{
$config = [
@@ -79,55 +98,7 @@ 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;
}
// login() kept for ClientCredentials compatibility (legacy/simple flow)
public function login(?string $clientId = null, ?string $clientSecret = null): static
{
// Allow overriding config via method arguments
@@ -144,9 +115,6 @@ class AlfaBankConnector extends Connector implements HasPagination
grantType: 'client_credentials'
);
// Send request (using self to reuse cert/ssl config)
// We temporarily disable auth middleware if we had any,
// but here we are raw, so it's fine.
$response = $this->send($request);
$data = $response->dto();
@@ -155,6 +123,7 @@ class AlfaBankConnector extends Connector implements HasPagination
return $this;
}
// </SECTION>
// </SECTION>
// <SECTION name="signer_accessor" intent="Provide access to the configured signing service for requests that require a digital signature.">
// <BELIEF_STATE "Requests should not instantiate their own signing logic. The connector acts as a service locator, providing the globally-configured Signer service. This ensures that all signatures are generated consistently.">

View File

@@ -33,6 +33,8 @@ readonly class AlfaConfig
public ?string $clientId = null,
// <BELIEF_STATE "Optional: The OAuth client secret, required for token-based authentication.">
public ?string $clientSecret = null,
// <BELIEF_STATE "Optional: The OAuth redirect URI, required for authorization code flow.">
public ?string $redirectUri = null,
// <BELIEF_STATE "Optional: Path to the certificate used specifically for signing requests (JWS/CMS), if different from the transport certificate.">
public ?string $signingCertificatePath = null,
// <BELIEF_STATE "Optional: Path to the private key used specifically for signing requests (JWS/CMS), if different from the transport key.">

View File

@@ -26,5 +26,8 @@ enum Environment: string
case SBP_PRODUCTION = 'https://pay.alfabank.ru/fsCryptoProxy';
case SBP_SANDBOX = 'https://217.12.103.132:2443/fsCryptoProxy';
// </SECTION>
case ID_PRODUCTION = 'https://id.alfabank.ru';
case ID_SANDBOX = 'https://id-sandbox.alfabank.ru';
}
// </SECTION>