instances added god may help
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user