forked from apoloz/alfa-bank-sdk
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:
72
README.md
72
README.md
@@ -1,5 +1,8 @@
|
||||
# Alfa Bank SDK (Unofficial)
|
||||
|
||||
[](https://github.com/apoloz/alfa-bank-sdk/actions)
|
||||
[](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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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...'
|
||||
];
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
70
examples/15_nominal_accounts_list.php
Normal file
70
examples/15_nominal_accounts_list.php
Normal 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";
|
||||
}
|
||||
89
examples/16_automated_startup.php
Normal file
89
examples/16_automated_startup.php
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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.">
|
||||
|
||||
@@ -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.">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user