158 lines
4.7 KiB
PHP
158 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXM\AuthenticationProviderOidc;
|
|
|
|
use KTXF\Security\Authentication\AuthenticationProviderAbstract;
|
|
use KTXF\Security\Authentication\ProviderContext;
|
|
use KTXF\Security\Authentication\ProviderResult;
|
|
|
|
/**
|
|
* OpenID Connect Identity Provider
|
|
*
|
|
* Implements OIDC authentication flow for SSO.
|
|
* Can be used as primary (redirect to IdP) or secondary (step-up authentication).
|
|
*/
|
|
class Provider extends AuthenticationProviderAbstract
|
|
{
|
|
public function __construct(
|
|
private readonly OidcClient $client,
|
|
) { }
|
|
|
|
// =========================================================================
|
|
// Provider Implementation
|
|
// =========================================================================
|
|
|
|
public function type(): string
|
|
{
|
|
return 'authentication';
|
|
}
|
|
|
|
public function identifier(): string
|
|
{
|
|
return 'oidc';
|
|
}
|
|
|
|
public function method(): string
|
|
{
|
|
return self::METHOD_REDIRECT;
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return 'OpenID Connect';
|
|
}
|
|
|
|
public function description(): string
|
|
{
|
|
return 'Authenticate using an OpenID Connect identity provider.';
|
|
}
|
|
|
|
public function icon(): string
|
|
{
|
|
return 'fa-solid fa-circle-o';
|
|
}
|
|
|
|
public function beginRedirect(ProviderContext $context, string $callbackUrl, ?string $returnUrl = null): ProviderResult
|
|
{
|
|
$config = $context->config;
|
|
|
|
if (empty($config)) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INVALID_PROVIDER,
|
|
'OIDC provider configuration is missing'
|
|
);
|
|
}
|
|
|
|
try {
|
|
$authData = $this->client->beginAuth($config, $callbackUrl, $returnUrl);
|
|
|
|
return ProviderResult::redirect(
|
|
$authData['redirect_url'],
|
|
[
|
|
'state' => $authData['state'],
|
|
'nonce' => $authData['nonce'] ?? null,
|
|
'return_url' => $returnUrl,
|
|
]
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INTERNAL,
|
|
'Failed to initiate OIDC authentication: ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
public function completeRedirect(ProviderContext $context, array $params): ProviderResult
|
|
{
|
|
$config = $context->config;
|
|
$expectedState = $context->getMeta('state');
|
|
$expectedNonce = $context->getMeta('nonce');
|
|
|
|
if (empty($config)) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INVALID_PROVIDER,
|
|
'OIDC provider configuration is missing'
|
|
);
|
|
}
|
|
|
|
if (empty($expectedState)) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INVALID_CREDENTIALS,
|
|
'Missing expected state for OIDC verification'
|
|
);
|
|
}
|
|
|
|
try {
|
|
$result = $this->client->completeAuth($params, $config, $expectedState, $expectedNonce);
|
|
|
|
if (!$result || !isset($result['success']) || !$result['success']) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INVALID_CREDENTIALS,
|
|
$result['error'] ?? 'OIDC authentication failed'
|
|
);
|
|
}
|
|
|
|
return ProviderResult::success(
|
|
[
|
|
'provider' => $this->identifier(),
|
|
'subject' => $result['sub'] ?? null,
|
|
'email' => $result['email'] ?? null,
|
|
'name' => $result['name'] ?? null,
|
|
'attributes' => $result['attributes'] ?? [],
|
|
],
|
|
[
|
|
'return_url' => $context->getMeta('return_url'),
|
|
]
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return ProviderResult::failed(
|
|
ProviderResult::ERROR_INTERNAL,
|
|
'Failed to complete OIDC authentication: ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Attribute Helpers
|
|
// =========================================================================
|
|
|
|
public function getAttributes(array $identity): array
|
|
{
|
|
return $identity['attributes'] ?? [];
|
|
}
|
|
|
|
public function mapAttributes(array $attributes, array $mapping): array
|
|
{
|
|
$mapped = [];
|
|
foreach ($mapping as $sourceKey => $targetKey) {
|
|
if (isset($attributes[$sourceKey])) {
|
|
$mapped[$targetKey] = $attributes[$sourceKey];
|
|
}
|
|
}
|
|
return $mapped;
|
|
}
|
|
|
|
}
|