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

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);
}
}