add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Service\Auth;
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Http\Request;
|
|
|
|
|
use MintyPHP\I18n;
|
|
|
|
|
|
|
|
|
|
class MicrosoftOidcService
|
|
|
|
|
{
|
|
|
|
|
private const CLOCK_SKEW = 120;
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
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
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$tenantId = (int) ($tenant['id'] ?? 0);
|
|
|
|
|
if ($tenantId <= 0) {
|
|
|
|
|
return ['ok' => false, 'error' => 'tenant_not_found'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$oidc = $this->fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!($oidc['ok'] ?? false)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'oidc_discovery_failed'];
|
|
|
|
|
}
|
|
|
|
|
$metadata = $oidc['config'] ?? [];
|
|
|
|
|
|
|
|
|
|
$state = self::randomString(32);
|
|
|
|
|
$nonce = self::randomString(32);
|
|
|
|
|
$codeVerifier = self::randomString(64);
|
|
|
|
|
$codeChallenge = self::base64UrlEncode(hash('sha256', $codeVerifier, true));
|
|
|
|
|
$locale = I18n::$locale ?? I18n::$defaultLocale ?? 'de';
|
|
|
|
|
$redirectUri = appUrl(Request::withLocale('auth/microsoft/callback', $locale));
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$this->stateStore->prune();
|
|
|
|
|
$this->stateStore->store($state, [
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'nonce' => $nonce,
|
|
|
|
|
'code_verifier' => $codeVerifier,
|
|
|
|
|
'locale' => $locale,
|
|
|
|
|
'redirect_uri' => $redirectUri,
|
2026-02-23 12:58:19 +01:00
|
|
|
]);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
|
|
|
|
$params = [
|
|
|
|
|
'client_id' => (string) ($providerConfig['client_id'] ?? ''),
|
|
|
|
|
'response_type' => 'code',
|
|
|
|
|
'redirect_uri' => $redirectUri,
|
|
|
|
|
'response_mode' => 'query',
|
2026-02-23 12:58:19 +01:00
|
|
|
'scope' => $this->buildAuthorizationScope($providerConfig),
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
'state' => $state,
|
|
|
|
|
'nonce' => $nonce,
|
|
|
|
|
'code_challenge' => $codeChallenge,
|
|
|
|
|
'code_challenge_method' => 'S256',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$authorizeEndpoint = (string) ($metadata['authorization_endpoint'] ?? '');
|
|
|
|
|
if ($authorizeEndpoint === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'authorize_endpoint_missing'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'url' => $authorizeEndpoint . '?' . http_build_query($params),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
public function handleCallback(string $state, string $code): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$state = trim($state);
|
|
|
|
|
$code = trim($code);
|
|
|
|
|
if ($state === '' || $code === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'callback_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$consumeResult = $this->stateStore->consume($state);
|
|
|
|
|
if (!$consumeResult['ok']) {
|
|
|
|
|
return ['ok' => false, 'error' => (string) ($consumeResult['error'] ?? 'state_invalid')];
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$entry = is_array($consumeResult['entry'] ?? null) ? $consumeResult['entry'] : [];
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$tenantId = (int) ($entry['tenant_id'] ?? 0);
|
2026-02-23 12:58:19 +01:00
|
|
|
$tenant = $this->tenantGateway->findById($tenantId);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
|
|
|
|
|
return ['ok' => false, 'error' => 'tenant_not_found'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$configResult = $this->tenantSsoService->getEffectiveMicrosoftProviderConfig($tenant);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!($configResult['ok'] ?? false)) {
|
|
|
|
|
return ['ok' => false, 'error' => (string) ($configResult['error'] ?? 'sso_config_invalid')];
|
|
|
|
|
}
|
|
|
|
|
$providerConfig = $configResult['config'] ?? [];
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$oidc = $this->fetchOpenIdConfiguration((string) ($providerConfig['authority'] ?? ''));
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!($oidc['ok'] ?? false)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'oidc_discovery_failed'];
|
|
|
|
|
}
|
|
|
|
|
$metadata = $oidc['config'] ?? [];
|
|
|
|
|
|
|
|
|
|
$tokenEndpoint = (string) ($metadata['token_endpoint'] ?? '');
|
|
|
|
|
if ($tokenEndpoint === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'token_endpoint_missing'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$tokenResponse = $this->oidcHttpGateway->call('POST', $tokenEndpoint, [
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
'grant_type' => 'authorization_code',
|
|
|
|
|
'client_id' => (string) ($providerConfig['client_id'] ?? ''),
|
|
|
|
|
'client_secret' => (string) ($providerConfig['client_secret'] ?? ''),
|
|
|
|
|
'code' => $code,
|
|
|
|
|
'redirect_uri' => (string) ($entry['redirect_uri'] ?? ''),
|
|
|
|
|
'code_verifier' => (string) ($entry['code_verifier'] ?? ''),
|
|
|
|
|
], [
|
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ((int) ($tokenResponse['status'] ?? 0) < 200 || (int) ($tokenResponse['status'] ?? 0) >= 300) {
|
|
|
|
|
return ['ok' => false, 'error' => 'token_exchange_failed'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tokenData = json_decode((string) ($tokenResponse['data'] ?? ''), true);
|
|
|
|
|
if (!is_array($tokenData)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'token_response_invalid'];
|
|
|
|
|
}
|
|
|
|
|
$idToken = trim((string) ($tokenData['id_token'] ?? ''));
|
|
|
|
|
if ($idToken === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_missing'];
|
|
|
|
|
}
|
|
|
|
|
$accessToken = trim((string) ($tokenData['access_token'] ?? ''));
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$validated = $this->validateIdToken(
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$idToken,
|
|
|
|
|
$providerConfig,
|
|
|
|
|
$metadata,
|
|
|
|
|
(string) ($entry['nonce'] ?? '')
|
|
|
|
|
);
|
|
|
|
|
if (!($validated['ok'] ?? false)) {
|
|
|
|
|
return ['ok' => false, 'error' => (string) ($validated['error'] ?? 'id_token_invalid')];
|
|
|
|
|
}
|
|
|
|
|
$claims = $validated['claims'] ?? [];
|
|
|
|
|
|
|
|
|
|
$email = self::extractEmailFromClaims($claims);
|
|
|
|
|
if ($email === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'email_missing'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$allowedDomainsCsv = (string) ($providerConfig['allowed_domains'] ?? '');
|
|
|
|
|
$allowedDomains = array_values(array_filter(array_map('trim', explode(',', $allowedDomainsCsv))));
|
2026-02-23 12:58:19 +01:00
|
|
|
if (!$this->tenantSsoService->isEmailDomainAllowed($email, $allowedDomains)) {
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
return ['ok' => false, 'error' => 'email_domain_not_allowed'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$syncEnabled = !empty($providerConfig['sync_profile_on_login']);
|
2026-02-23 12:58:19 +01:00
|
|
|
$syncFields = $this->tenantSsoService->normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if ($syncEnabled && !$syncFields) {
|
2026-02-23 12:58:19 +01:00
|
|
|
$syncFields = $this->tenantSsoService->defaultProfileSyncFields();
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
}
|
|
|
|
|
$graphProfile = [];
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($syncEnabled && $this->tenantSsoService->profileSyncFieldsRequireGraph($syncFields) && $accessToken !== '') {
|
|
|
|
|
$graph = $this->fetchMicrosoftGraphProfile($accessToken);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!empty($graph['ok'])) {
|
|
|
|
|
$graphProfile = is_array($graph['profile'] ?? null) ? $graph['profile'] : [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$graphAvatar = [];
|
|
|
|
|
if ($syncEnabled && in_array('avatar', $syncFields, true) && $accessToken !== '') {
|
2026-02-23 12:58:19 +01:00
|
|
|
$avatar = $this->fetchMicrosoftGraphAvatar($accessToken);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if (!empty($avatar['ok'])) {
|
|
|
|
|
$graphAvatar = is_array($avatar['avatar'] ?? null) ? $avatar['avatar'] : [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'tenant' => $tenant,
|
|
|
|
|
'locale' => (string) ($entry['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale ?? 'de')),
|
|
|
|
|
'graph_profile' => $graphProfile,
|
|
|
|
|
'graph_avatar' => $graphAvatar,
|
|
|
|
|
'sync_config' => [
|
|
|
|
|
'enabled' => $syncEnabled,
|
|
|
|
|
'fields' => $syncFields,
|
|
|
|
|
],
|
|
|
|
|
'claims' => [
|
|
|
|
|
'tid' => (string) ($claims['tid'] ?? ''),
|
|
|
|
|
'oid' => (string) ($claims['oid'] ?? ''),
|
|
|
|
|
'issuer' => (string) ($claims['iss'] ?? ''),
|
|
|
|
|
'subject' => (string) ($claims['sub'] ?? ''),
|
|
|
|
|
'email' => $email,
|
|
|
|
|
'name' => trim((string) ($claims['name'] ?? '')),
|
|
|
|
|
'given_name' => trim((string) ($claims['given_name'] ?? '')),
|
|
|
|
|
'family_name' => trim((string) ($claims['family_name'] ?? '')),
|
|
|
|
|
'preferred_language' => trim((string) ($claims['preferred_language'] ?? '')),
|
|
|
|
|
'graph_given_name' => trim((string) ($graphProfile['given_name'] ?? '')),
|
|
|
|
|
'graph_family_name' => trim((string) ($graphProfile['family_name'] ?? '')),
|
|
|
|
|
'graph_phone' => trim((string) ($graphProfile['phone'] ?? '')),
|
|
|
|
|
'graph_mobile' => trim((string) ($graphProfile['mobile'] ?? '')),
|
|
|
|
|
'graph_avatar_mime' => trim((string) ($graphAvatar['mime'] ?? '')),
|
|
|
|
|
'graph_avatar_data_base64' => trim((string) ($graphAvatar['data_base64'] ?? '')),
|
|
|
|
|
'sync_profile_on_login' => $syncEnabled ? 1 : 0,
|
|
|
|
|
'sync_profile_fields' => $syncFields,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function buildAuthorizationScope(array $providerConfig): string
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$scopes = ['openid', 'profile', 'email'];
|
|
|
|
|
$syncEnabled = !empty($providerConfig['sync_profile_on_login']);
|
2026-02-23 12:58:19 +01:00
|
|
|
$syncFields = $this->tenantSsoService->normalizeProfileSyncFields($providerConfig['sync_profile_fields'] ?? []);
|
|
|
|
|
if ($syncEnabled && $this->tenantSsoService->profileSyncFieldsRequireGraph($syncFields)) {
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
$scopes[] = 'User.Read';
|
|
|
|
|
}
|
|
|
|
|
$scopes = array_values(array_unique($scopes));
|
|
|
|
|
return implode(' ', $scopes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function fetchOpenIdConfiguration(string $authority): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$authority = trim($authority);
|
|
|
|
|
if ($authority === '') {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
$url = rtrim($authority, '/') . '/.well-known/openid-configuration';
|
2026-02-23 12:58:19 +01:00
|
|
|
$response = $this->oidcHttpGateway->call('GET', $url, '', ['Accept' => 'application/json']);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if ((int) ($response['status'] ?? 0) < 200 || (int) ($response['status'] ?? 0) >= 300) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
$config = json_decode((string) ($response['data'] ?? ''), true);
|
|
|
|
|
if (!is_array($config)) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
return ['ok' => true, 'config' => $config];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function validateIdToken(
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
string $idToken,
|
|
|
|
|
array $providerConfig,
|
|
|
|
|
array $metadata,
|
|
|
|
|
string $expectedNonce
|
|
|
|
|
): array {
|
|
|
|
|
$parts = explode('.', $idToken);
|
|
|
|
|
if (count($parts) !== 3) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_format_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$header = json_decode(self::base64UrlDecode($parts[0]), true);
|
|
|
|
|
$claims = json_decode(self::base64UrlDecode($parts[1]), true);
|
|
|
|
|
$signature = self::base64UrlDecode($parts[2]);
|
|
|
|
|
if (!is_array($header) || !is_array($claims) || $signature === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_parse_failed'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$alg = strtoupper(trim((string) ($header['alg'] ?? '')));
|
|
|
|
|
if ($alg !== 'RS256') {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_alg_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$jwksUri = trim((string) ($metadata['jwks_uri'] ?? ''));
|
|
|
|
|
if ($jwksUri === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'jwks_uri_missing'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
$jwksResponse = $this->oidcHttpGateway->call('GET', $jwksUri, '', ['Accept' => 'application/json']);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
if ((int) ($jwksResponse['status'] ?? 0) < 200 || (int) ($jwksResponse['status'] ?? 0) >= 300) {
|
|
|
|
|
return ['ok' => false, 'error' => 'jwks_fetch_failed'];
|
|
|
|
|
}
|
|
|
|
|
$jwks = json_decode((string) ($jwksResponse['data'] ?? ''), true);
|
|
|
|
|
if (!is_array($jwks) || !is_array($jwks['keys'] ?? null)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'jwks_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$kid = (string) ($header['kid'] ?? '');
|
|
|
|
|
$jwk = null;
|
|
|
|
|
foreach ($jwks['keys'] as $key) {
|
|
|
|
|
if (!is_array($key)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ((string) ($key['kid'] ?? '') === $kid) {
|
|
|
|
|
$jwk = $key;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!$jwk) {
|
|
|
|
|
return ['ok' => false, 'error' => 'jwk_not_found'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pem = self::jwkToPem($jwk);
|
|
|
|
|
if ($pem === null) {
|
|
|
|
|
return ['ok' => false, 'error' => 'jwk_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$verified = openssl_verify(
|
|
|
|
|
$parts[0] . '.' . $parts[1],
|
|
|
|
|
$signature,
|
|
|
|
|
$pem,
|
|
|
|
|
OPENSSL_ALGO_SHA256
|
|
|
|
|
);
|
|
|
|
|
if ($verified !== 1) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_signature_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$now = time();
|
|
|
|
|
$exp = (int) ($claims['exp'] ?? 0);
|
|
|
|
|
if ($exp <= 0 || $exp < ($now - self::CLOCK_SKEW)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_expired'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$nbf = (int) ($claims['nbf'] ?? 0);
|
|
|
|
|
if ($nbf > 0 && $nbf > ($now + self::CLOCK_SKEW)) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_not_yet_valid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$audience = $claims['aud'] ?? '';
|
|
|
|
|
$clientId = (string) ($providerConfig['client_id'] ?? '');
|
|
|
|
|
$audValid = false;
|
|
|
|
|
if (is_array($audience)) {
|
|
|
|
|
$audValid = in_array($clientId, $audience, true);
|
|
|
|
|
} else {
|
|
|
|
|
$audValid = (string) $audience === $clientId;
|
|
|
|
|
}
|
|
|
|
|
if (!$audValid) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_aud_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((string) ($claims['nonce'] ?? '') !== $expectedNonce) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_nonce_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tid = strtolower((string) ($claims['tid'] ?? ''));
|
|
|
|
|
$expectedTid = strtolower((string) ($providerConfig['tenant_id'] ?? ''));
|
|
|
|
|
if ($expectedTid === '' || $tid !== $expectedTid) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_tid_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$iss = strtolower((string) ($claims['iss'] ?? ''));
|
|
|
|
|
$issuerTemplate = strtolower((string) ($metadata['issuer'] ?? ''));
|
|
|
|
|
if ($issuerTemplate !== '') {
|
|
|
|
|
$expectedIssuer = str_replace('{tenantid}', $tid, $issuerTemplate);
|
|
|
|
|
if ($iss !== $expectedIssuer) {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_iss_invalid'];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((string) ($claims['oid'] ?? '') === '' || (string) ($claims['sub'] ?? '') === '') {
|
|
|
|
|
return ['ok' => false, 'error' => 'id_token_identity_invalid'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['ok' => true, 'claims' => $claims];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function extractEmailFromClaims(array $claims): string
|
|
|
|
|
{
|
|
|
|
|
$email = trim((string) ($claims['email'] ?? ''));
|
|
|
|
|
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
|
|
|
return strtolower($email);
|
|
|
|
|
}
|
|
|
|
|
$upn = trim((string) ($claims['preferred_username'] ?? ''));
|
|
|
|
|
if ($upn !== '' && filter_var($upn, FILTER_VALIDATE_EMAIL)) {
|
|
|
|
|
return strtolower($upn);
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function fetchMicrosoftGraphProfile(string $accessToken): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,mobilePhone,businessPhones';
|
2026-02-23 12:58:19 +01:00
|
|
|
$response = $this->oidcHttpGateway->call('GET', $url, '', [
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
'Accept' => 'application/json',
|
|
|
|
|
'Authorization' => 'Bearer ' . $accessToken,
|
|
|
|
|
]);
|
|
|
|
|
$status = (int) ($response['status'] ?? 0);
|
|
|
|
|
if ($status < 200 || $status >= 300) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
$data = json_decode((string) ($response['data'] ?? ''), true);
|
|
|
|
|
if (!is_array($data)) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$phone = '';
|
|
|
|
|
$businessPhones = $data['businessPhones'] ?? [];
|
|
|
|
|
if (is_array($businessPhones) && isset($businessPhones[0])) {
|
|
|
|
|
$phone = trim((string) $businessPhones[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'profile' => [
|
|
|
|
|
'given_name' => trim((string) ($data['givenName'] ?? '')),
|
|
|
|
|
'family_name' => trim((string) ($data['surname'] ?? '')),
|
|
|
|
|
'mobile' => trim((string) ($data['mobilePhone'] ?? '')),
|
|
|
|
|
'phone' => $phone,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
private function fetchMicrosoftGraphAvatar(string $accessToken): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
{
|
|
|
|
|
$url = 'https://graph.microsoft.com/v1.0/me/photo/$value';
|
2026-02-23 12:58:19 +01:00
|
|
|
$response = $this->oidcHttpGateway->call('GET', $url, '', [
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
'Accept' => 'image/*',
|
|
|
|
|
'Authorization' => 'Bearer ' . $accessToken,
|
|
|
|
|
]);
|
|
|
|
|
$status = (int) ($response['status'] ?? 0);
|
|
|
|
|
if ($status < 200 || $status >= 300) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = $response['data'] ?? null;
|
|
|
|
|
if (!is_string($data) || $data === '') {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$contentType = self::headerValue($response, 'Content-Type');
|
|
|
|
|
$contentType = strtolower(trim(explode(';', $contentType, 2)[0]));
|
|
|
|
|
if (!in_array($contentType, ['image/jpeg', 'image/png', 'image/webp'], true)) {
|
|
|
|
|
return ['ok' => false];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'avatar' => [
|
|
|
|
|
'mime' => $contentType,
|
|
|
|
|
'data_base64' => base64_encode($data),
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function headerValue(array $response, string $name): string
|
|
|
|
|
{
|
|
|
|
|
$headers = is_array($response['headers'] ?? null) ? $response['headers'] : [];
|
|
|
|
|
foreach ($headers as $key => $value) {
|
|
|
|
|
if (strcasecmp((string) $key, $name) === 0) {
|
|
|
|
|
return is_string($value) ? $value : '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function randomString(int $bytes): string
|
|
|
|
|
{
|
|
|
|
|
return self::base64UrlEncode(random_bytes($bytes));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function base64UrlEncode(string $value): string
|
|
|
|
|
{
|
|
|
|
|
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function base64UrlDecode(string $value): string
|
|
|
|
|
{
|
|
|
|
|
$value = strtr($value, '-_', '+/');
|
|
|
|
|
$pad = strlen($value) % 4;
|
|
|
|
|
if ($pad > 0) {
|
|
|
|
|
$value .= str_repeat('=', 4 - $pad);
|
|
|
|
|
}
|
|
|
|
|
$decoded = base64_decode($value, true);
|
|
|
|
|
return is_string($decoded) ? $decoded : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function jwkToPem(array $jwk): ?string
|
|
|
|
|
{
|
|
|
|
|
$n = self::base64UrlDecode((string) ($jwk['n'] ?? ''));
|
|
|
|
|
$e = self::base64UrlDecode((string) ($jwk['e'] ?? ''));
|
|
|
|
|
if ($n === '' || $e === '') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$modulus = self::asn1Integer($n);
|
|
|
|
|
$publicExponent = self::asn1Integer($e);
|
|
|
|
|
$rsaPublicKey = self::asn1Sequence($modulus . $publicExponent);
|
|
|
|
|
|
|
|
|
|
$algo = self::asn1Sequence(
|
|
|
|
|
self::asn1ObjectIdentifier("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") .
|
|
|
|
|
self::asn1Null()
|
|
|
|
|
);
|
|
|
|
|
$bitString = "\x03" . self::asn1Length(strlen($rsaPublicKey) + 1) . "\x00" . $rsaPublicKey;
|
|
|
|
|
$subjectPublicKeyInfo = self::asn1Sequence($algo . $bitString);
|
|
|
|
|
|
|
|
|
|
$pem = "-----BEGIN PUBLIC KEY-----\n";
|
|
|
|
|
$pem .= chunk_split(base64_encode($subjectPublicKeyInfo), 64, "\n");
|
|
|
|
|
$pem .= "-----END PUBLIC KEY-----\n";
|
|
|
|
|
return $pem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function asn1Integer(string $bytes): string
|
|
|
|
|
{
|
|
|
|
|
if (ord($bytes[0]) > 0x7F) {
|
|
|
|
|
$bytes = "\x00" . $bytes;
|
|
|
|
|
}
|
|
|
|
|
return "\x02" . self::asn1Length(strlen($bytes)) . $bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function asn1Sequence(string $bytes): string
|
|
|
|
|
{
|
|
|
|
|
return "\x30" . self::asn1Length(strlen($bytes)) . $bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function asn1ObjectIdentifier(string $bytes): string
|
|
|
|
|
{
|
|
|
|
|
return "\x06" . self::asn1Length(strlen($bytes)) . $bytes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function asn1Null(): string
|
|
|
|
|
{
|
|
|
|
|
return "\x05\x00";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function asn1Length(int $length): string
|
|
|
|
|
{
|
|
|
|
|
if ($length < 128) {
|
|
|
|
|
return chr($length);
|
|
|
|
|
}
|
|
|
|
|
$temp = ltrim(pack('N', $length), "\x00");
|
|
|
|
|
return chr(0x80 | strlen($temp)) . $temp;
|
|
|
|
|
}
|
|
|
|
|
}
|