instances added god may help

This commit is contained in:
2026-02-23 12:58:19 +01:00
parent 25370a1a55
commit 99db252f60
290 changed files with 9858 additions and 4914 deletions

View File

@@ -2,25 +2,29 @@
namespace MintyPHP\Service\Auth;
use MintyPHP\Curl;
use MintyPHP\Http\Request;
use MintyPHP\I18n;
use MintyPHP\Repository\Tenant\TenantRepository;
class MicrosoftOidcService
{
private const SESSION_KEY = 'microsoft_oidc_states';
private const STATE_TTL = 600;
private const CLOCK_SKEW = 120;
public static function startAuthorization(array $tenant, array $providerConfig): array
public function __construct(
private readonly TenantSsoService $tenantSsoService,
private readonly AuthTenantGateway $tenantGateway,
private readonly OidcHttpGateway $oidcHttpGateway,
private readonly MicrosoftOidcStateStoreService $stateStore
) {
}
public function startAuthorization(array $tenant, array $providerConfig): array
{
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
return ['ok' => false, 'error' => 'tenant_not_found'];
}
$oidc = self::fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
$oidc = $this->fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
if (!($oidc['ok'] ?? false)) {
return ['ok' => false, 'error' => 'oidc_discovery_failed'];
}
@@ -33,22 +37,21 @@ class MicrosoftOidcService
$locale = I18n::$locale ?? I18n::$defaultLocale ?? 'de';
$redirectUri = appUrl(Request::withLocale('auth/microsoft/callback', $locale));
self::pruneStates();
$_SESSION[self::SESSION_KEY][$state] = [
$this->stateStore->prune();
$this->stateStore->store($state, [
'tenant_id' => $tenantId,
'nonce' => $nonce,
'code_verifier' => $codeVerifier,
'locale' => $locale,
'redirect_uri' => $redirectUri,
'created_at' => time(),
];
]);
$params = [
'client_id' => (string) ($providerConfig['client_id'] ?? ''),
'response_type' => 'code',
'redirect_uri' => $redirectUri,
'response_mode' => 'query',
'scope' => self::buildAuthorizationScope($providerConfig),
'scope' => $this->buildAuthorizationScope($providerConfig),
'state' => $state,
'nonce' => $nonce,
'code_challenge' => $codeChallenge,
@@ -66,7 +69,7 @@ class MicrosoftOidcService
];
}
public static function handleCallback(string $state, string $code): array
public function handleCallback(string $state, string $code): array
{
$state = trim($state);
$code = trim($code);
@@ -74,31 +77,25 @@ class MicrosoftOidcService
return ['ok' => false, 'error' => 'callback_invalid'];
}
$states = $_SESSION[self::SESSION_KEY] ?? [];
$entry = is_array($states[$state] ?? null) ? $states[$state] : null;
if (!$entry) {
return ['ok' => false, 'error' => 'state_invalid'];
}
unset($_SESSION[self::SESSION_KEY][$state]);
$createdAt = (int) ($entry['created_at'] ?? 0);
if ($createdAt <= 0 || (time() - $createdAt) > self::STATE_TTL) {
return ['ok' => false, 'error' => 'state_expired'];
$consumeResult = $this->stateStore->consume($state);
if (!$consumeResult['ok']) {
return ['ok' => false, 'error' => (string) ($consumeResult['error'] ?? 'state_invalid')];
}
$entry = is_array($consumeResult['entry'] ?? null) ? $consumeResult['entry'] : [];
$tenantId = (int) ($entry['tenant_id'] ?? 0);
$tenant = TenantRepository::find($tenantId);
$tenant = $this->tenantGateway->findById($tenantId);
if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
return ['ok' => false, 'error' => 'tenant_not_found'];
}
$configResult = TenantSsoService::getEffectiveMicrosoftProviderConfig($tenant);
$configResult = $this->tenantSsoService->getEffectiveMicrosoftProviderConfig($tenant);
if (!($configResult['ok'] ?? false)) {
return ['ok' => false, 'error' => (string) ($configResult['error'] ?? 'sso_config_invalid')];
}
$providerConfig = $configResult['config'] ?? [];
$oidc = self::fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
$oidc = $this->fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
if (!($oidc['ok'] ?? false)) {
return ['ok' => false, 'error' => 'oidc_discovery_failed'];
}
@@ -109,7 +106,7 @@ class MicrosoftOidcService
return ['ok' => false, 'error' => 'token_endpoint_missing'];
}
$tokenResponse = Curl::call('POST', $tokenEndpoint, [
$tokenResponse = $this->oidcHttpGateway->call('POST', $tokenEndpoint, [
'grant_type' => 'authorization_code',
'client_id' => (string) ($providerConfig['client_id'] ?? ''),
'client_secret' => (string) ($providerConfig['client_secret'] ?? ''),
@@ -134,7 +131,7 @@ class MicrosoftOidcService
}
$accessToken = trim((string) ($tokenData['access_token'] ?? ''));
$validated = self::validateIdToken(
$validated = $this->validateIdToken(
$idToken,
$providerConfig,
$metadata,
@@ -152,25 +149,25 @@ class MicrosoftOidcService
$allowedDomainsCsv = (string) ($providerConfig['allowed_domains'] ?? '');
$allowedDomains = array_values(array_filter(array_map('trim', explode(',', $allowedDomainsCsv))));
if (!TenantSsoService::isEmailDomainAllowed($email, $allowedDomains)) {
if (!$this->tenantSsoService->isEmailDomainAllowed($email, $allowedDomains)) {
return ['ok' => false, 'error' => 'email_domain_not_allowed'];
}
$syncEnabled = !empty($providerConfig['sync_profile_on_login']);
$syncFields = TenantSsoService::normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
$syncFields = $this->tenantSsoService->normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
if ($syncEnabled && !$syncFields) {
$syncFields = TenantSsoService::defaultProfileSyncFields();
$syncFields = $this->tenantSsoService->defaultProfileSyncFields();
}
$graphProfile = [];
if ($syncEnabled && TenantSsoService::profileSyncFieldsRequireGraph($syncFields) && $accessToken !== '') {
$graph = self::fetchMicrosoftGraphProfile($accessToken);
if ($syncEnabled && $this->tenantSsoService->profileSyncFieldsRequireGraph($syncFields) && $accessToken !== '') {
$graph = $this->fetchMicrosoftGraphProfile($accessToken);
if (!empty($graph['ok'])) {
$graphProfile = is_array($graph['profile'] ?? null) ? $graph['profile'] : [];
}
}
$graphAvatar = [];
if ($syncEnabled && in_array('avatar', $syncFields, true) && $accessToken !== '') {
$avatar = self::fetchMicrosoftGraphAvatar($accessToken);
$avatar = $this->fetchMicrosoftGraphAvatar($accessToken);
if (!empty($avatar['ok'])) {
$graphAvatar = is_array($avatar['avatar'] ?? null) ? $avatar['avatar'] : [];
}
@@ -208,26 +205,26 @@ class MicrosoftOidcService
];
}
private static function buildAuthorizationScope(array $providerConfig): string
private function buildAuthorizationScope(array $providerConfig): string
{
$scopes = ['openid', 'profile', 'email'];
$syncEnabled = !empty($providerConfig['sync_profile_on_login']);
$syncFields = TenantSsoService::normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
if ($syncEnabled && TenantSsoService::profileSyncFieldsRequireGraph($syncFields)) {
$syncFields = $this->tenantSsoService->normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
if ($syncEnabled && $this->tenantSsoService->profileSyncFieldsRequireGraph($syncFields)) {
$scopes[] = 'User.Read';
}
$scopes = array_values(array_unique($scopes));
return implode(' ', $scopes);
}
private static function fetchOpenIdConfiguration(string $authority): array
private function fetchOpenIdConfiguration(string $authority): array
{
$authority = trim($authority);
if ($authority === '') {
return ['ok' => false];
}
$url = rtrim($authority, '/') . '/.well-known/openid-configuration';
$response = Curl::call('GET', $url, '', ['Accept' => 'application/json']);
$response = $this->oidcHttpGateway->call('GET', $url, '', ['Accept' => 'application/json']);
if ((int) ($response['status'] ?? 0) < 200 || (int) ($response['status'] ?? 0) >= 300) {
return ['ok' => false];
}
@@ -238,7 +235,7 @@ class MicrosoftOidcService
return ['ok' => true, 'config' => $config];
}
private static function validateIdToken(
private function validateIdToken(
string $idToken,
array $providerConfig,
array $metadata,
@@ -266,7 +263,7 @@ class MicrosoftOidcService
return ['ok' => false, 'error' => 'jwks_uri_missing'];
}
$jwksResponse = Curl::call('GET', $jwksUri, '', ['Accept' => 'application/json']);
$jwksResponse = $this->oidcHttpGateway->call('GET', $jwksUri, '', ['Accept' => 'application/json']);
if ((int) ($jwksResponse['status'] ?? 0) < 200 || (int) ($jwksResponse['status'] ?? 0) >= 300) {
return ['ok' => false, 'error' => 'jwks_fetch_failed'];
}
@@ -367,10 +364,10 @@ class MicrosoftOidcService
return '';
}
private static function fetchMicrosoftGraphProfile(string $accessToken): array
private function fetchMicrosoftGraphProfile(string $accessToken): array
{
$url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,mobilePhone,businessPhones';
$response = Curl::call('GET', $url, '', [
$response = $this->oidcHttpGateway->call('GET', $url, '', [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $accessToken,
]);
@@ -400,10 +397,10 @@ class MicrosoftOidcService
];
}
private static function fetchMicrosoftGraphAvatar(string $accessToken): array
private function fetchMicrosoftGraphAvatar(string $accessToken): array
{
$url = 'https://graph.microsoft.com/v1.0/me/photo/$value';
$response = Curl::call('GET', $url, '', [
$response = $this->oidcHttpGateway->call('GET', $url, '', [
'Accept' => 'image/*',
'Authorization' => 'Bearer ' . $accessToken,
]);
@@ -443,23 +440,6 @@ class MicrosoftOidcService
return '';
}
private static function pruneStates(): void
{
$states = $_SESSION[self::SESSION_KEY] ?? [];
if (!is_array($states)) {
$_SESSION[self::SESSION_KEY] = [];
return;
}
$now = time();
foreach ($states as $state => $payload) {
$createdAt = (int) (($payload['created_at'] ?? 0));
if ($createdAt <= 0 || ($now - $createdAt) > self::STATE_TTL) {
unset($states[$state]);
}
}
$_SESSION[self::SESSION_KEY] = $states;
}
private static function randomString(int $bytes): string
{
return self::base64UrlEncode(random_bytes($bytes));