From fea37ffec53aba70f06f448b847fdb9927bae9f0 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 25 Sep 2026 10:44:44 +0200 Subject: [PATCH 1/2] feat(oauth): Add necessary interfaces to support per-service OAuth --- .../ServiceAuthorizationRepositoryFactory.php | 18 +++++ .../ServiceAuthorizationServiceFactory.php | 18 +++++ src/Factory/TokenGrantRepositoryFactory.php | 18 +++++ .../Exception/OAuthTokenRefreshException.php | 30 +++++++- src/Service/GrantStrategy.php | 22 ++++++ .../NullServiceAuthorizationRepository.php | 33 +++++++++ .../NullServiceAuthorizationService.php | 53 +++++++++++++ src/Service/NullTokenGrantRepository.php | 30 ++++++++ src/Service/OAuthProviderConfigRepository.php | 2 +- src/Service/ServiceAuthorization.php | 29 ++++++++ .../ServiceAuthorizationRepository.php | 26 +++++++ src/Service/ServiceAuthorizationService.php | 74 +++++++++++++++++++ src/Service/ServiceNotAuthorizedException.php | 51 +++++++++++++ src/Service/ServicePurpose.php | 50 +++++++++++++ src/Service/TokenGrant.php | 36 +++++++++ src/Service/TokenGrantRepository.php | 32 ++++++++ src/Service/UnsupportedPurposeException.php | 35 +++++++++ 17 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 src/Factory/ServiceAuthorizationRepositoryFactory.php create mode 100644 src/Factory/ServiceAuthorizationServiceFactory.php create mode 100644 src/Factory/TokenGrantRepositoryFactory.php create mode 100644 src/Service/GrantStrategy.php create mode 100644 src/Service/NullServiceAuthorizationRepository.php create mode 100644 src/Service/NullServiceAuthorizationService.php create mode 100644 src/Service/NullTokenGrantRepository.php create mode 100644 src/Service/ServiceAuthorization.php create mode 100644 src/Service/ServiceAuthorizationRepository.php create mode 100644 src/Service/ServiceAuthorizationService.php create mode 100644 src/Service/ServiceNotAuthorizedException.php create mode 100644 src/Service/ServicePurpose.php create mode 100644 src/Service/TokenGrant.php create mode 100644 src/Service/TokenGrantRepository.php create mode 100644 src/Service/UnsupportedPurposeException.php diff --git a/src/Factory/ServiceAuthorizationRepositoryFactory.php b/src/Factory/ServiceAuthorizationRepositoryFactory.php new file mode 100644 index 00000000..71ddee87 --- /dev/null +++ b/src/Factory/ServiceAuthorizationRepositoryFactory.php @@ -0,0 +1,18 @@ +userId; + } + + public function providerId(): string + { + return $this->providerId; + } + + public function grant(): ?TokenGrant + { + return $this->grant; + } +} diff --git a/src/Service/GrantStrategy.php b/src/Service/GrantStrategy.php new file mode 100644 index 00000000..8c5ae703 --- /dev/null +++ b/src/Service/GrantStrategy.php @@ -0,0 +1,22 @@ +getAccessToken(). + * + * @throws ServiceNotAuthorizedException when !isSatisfied() + * @throws OAuthTokenRefreshException when the grant refresh fails + */ + public function getAccessToken(): string; +} diff --git a/src/Service/ServiceAuthorizationRepository.php b/src/Service/ServiceAuthorizationRepository.php new file mode 100644 index 00000000..4f3f943a --- /dev/null +++ b/src/Service/ServiceAuthorizationRepository.php @@ -0,0 +1,26 @@ +identifier(), + $providerId + ); + } + parent::__construct($message, $code, $previous); + } + + public function userId(): string + { + return $this->userId; + } + + public function providerId(): string + { + return $this->providerId; + } + + public function purpose(): ServicePurpose + { + return $this->purpose; + } + + /** Scopes still needed; empty ScopeSet if no grant exists at all. */ + public function missingScopes(): ScopeSet + { + return $this->missingScopes; + } +} diff --git a/src/Service/ServicePurpose.php b/src/Service/ServicePurpose.php new file mode 100644 index 00000000..006aa196 --- /dev/null +++ b/src/Service/ServicePurpose.php @@ -0,0 +1,50 @@ +identifier; + } + + public function grantStrategy(): GrantStrategy + { + return $this->grantStrategy; + } + + public function equals(self $other): bool + { + return $this->identifier === $other->identifier; + } + + /** Round-trip through OAuthFlowData persistence. */ + public function serialize(): array + { + return ['id' => $this->identifier, 'strategy' => $this->grantStrategy->value]; + } + + public static function deserialize(array $data): self + { + return new self($data['id'], GrantStrategy::from($data['strategy'])); + } +} diff --git a/src/Service/TokenGrant.php b/src/Service/TokenGrant.php new file mode 100644 index 00000000..3d1558fe --- /dev/null +++ b/src/Service/TokenGrant.php @@ -0,0 +1,36 @@ +identifier() + ); + } + parent::__construct($message, $code, $previous); + } + + public function providerId(): string + { + return $this->providerId; + } + + public function purpose(): ServicePurpose + { + return $this->purpose; + } +} From 351357b57075d8feaa30e8df32c1c5a33c39fd74 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Mon, 28 Sep 2026 11:04:31 +0200 Subject: [PATCH 2/2] feat: Interfaces for Password-like Service Credentials handling --- src/DefaultInjectorBindings.php | 12 ++++ src/Service/AcquisitionAction.php | 18 +++++ src/Service/CredentialAcquisitionResult.php | 19 +++++ .../CredentialProvisioningStrategy.php | 24 +++++++ src/Service/CredentialStore.php | 68 ++++++++++++++++++ src/Service/CredentialType.php | 16 +++++ .../CredentialProvisioningStrategyFactory.php | 22 ++++++ .../Factory/CredentialStoreFactory.php | 22 ++++++ ...wordServiceAuthorizationServiceFactory.php | 22 ++++++ .../ServiceCredentialManagerFactory.php | 22 ++++++ .../NullCredentialProvisioningStrategy.php | 23 ++++++ src/Service/NullCredentialStore.php | 62 ++++++++++++++++ ...ullPasswordServiceAuthorizationService.php | 61 ++++++++++++++++ src/Service/NullServiceCredentialManager.php | 40 +++++++++++ src/Service/PasswordCredential.php | 47 ++++++++++++ .../PasswordCredentialNotFoundException.php | 47 ++++++++++++ .../PasswordServiceAuthorizationService.php | 72 +++++++++++++++++++ src/Service/ProvisioningAction.php | 18 +++++ src/Service/ProvisioningResult.php | 19 +++++ src/Service/ServiceCredentialManager.php | 43 +++++++++++ src/Service/ServiceCredentialResult.php | 38 ++++++++++ 21 files changed, 715 insertions(+) create mode 100644 src/Service/AcquisitionAction.php create mode 100644 src/Service/CredentialAcquisitionResult.php create mode 100644 src/Service/CredentialProvisioningStrategy.php create mode 100644 src/Service/CredentialStore.php create mode 100644 src/Service/CredentialType.php create mode 100644 src/Service/Factory/CredentialProvisioningStrategyFactory.php create mode 100644 src/Service/Factory/CredentialStoreFactory.php create mode 100644 src/Service/Factory/PasswordServiceAuthorizationServiceFactory.php create mode 100644 src/Service/Factory/ServiceCredentialManagerFactory.php create mode 100644 src/Service/NullCredentialProvisioningStrategy.php create mode 100644 src/Service/NullCredentialStore.php create mode 100644 src/Service/NullPasswordServiceAuthorizationService.php create mode 100644 src/Service/NullServiceCredentialManager.php create mode 100644 src/Service/PasswordCredential.php create mode 100644 src/Service/PasswordCredentialNotFoundException.php create mode 100644 src/Service/PasswordServiceAuthorizationService.php create mode 100644 src/Service/ProvisioningAction.php create mode 100644 src/Service/ProvisioningResult.php create mode 100644 src/Service/ServiceCredentialManager.php create mode 100644 src/Service/ServiceCredentialResult.php diff --git a/src/DefaultInjectorBindings.php b/src/DefaultInjectorBindings.php index 5f9da666..e87385f0 100644 --- a/src/DefaultInjectorBindings.php +++ b/src/DefaultInjectorBindings.php @@ -78,6 +78,10 @@ use Horde\Core\Factory\OAuthFlowStoreFactory; use Horde\Core\Factory\PermissionServiceFactory; use Horde\Core\Factory\PrefsServiceFactory; +use Horde\Core\Service\Factory\CredentialStoreFactory; +use Horde\Core\Service\Factory\CredentialProvisioningStrategyFactory; +use Horde\Core\Service\Factory\PasswordServiceAuthorizationServiceFactory; +use Horde\Core\Service\Factory\ServiceCredentialManagerFactory; use Horde\Core\Factory\RegistryConfigLoaderFactory; use Horde\Core\Factory\RouteUrlWriterFactory; use Horde\Core\Factory\RuntimeRoutesProviderFactory; @@ -94,6 +98,8 @@ use Horde\Core\Middleware\ErrorFilter; use Horde\Core\Middleware\OAuthConsentMiddleware; use Horde\Core\Service\ApplicationService; +use Horde\Core\Service\CredentialStore; +use Horde\Core\Service\CredentialProvisioningStrategy; use Horde\Core\Service\GroupService; use Horde\Core\Service\HordeDbService; use Horde\Core\Service\HordeLdapService; @@ -101,8 +107,10 @@ use Horde\Core\Service\OAuthHttpClientService; use Horde\Core\Service\OAuthProviderConfigRepository; use Horde\Core\Service\OAuthTokenService; +use Horde\Core\Service\PasswordServiceAuthorizationService; use Horde\Core\Service\PermissionService; use Horde\Core\Service\PrefsService; +use Horde\Core\Service\ServiceCredentialManager; use Horde\Core\Service\VersionCheck\VersionService; use Horde\Core\Uri\RegistryRouteMapperProvider; use Horde\Core\Uri\RouteMapperProvider; @@ -288,6 +296,10 @@ public function register(Injector $injector): void HordeLdapService::class => HordeLdapServiceFactory::class, PermissionService::class => PermissionServiceFactory::class, VersionService::class => VersionServiceFactory::class, + CredentialStore::class => CredentialStoreFactory::class, + CredentialProvisioningStrategy::class => CredentialProvisioningStrategyFactory::class, + PasswordServiceAuthorizationService::class => PasswordServiceAuthorizationServiceFactory::class, + ServiceCredentialManager::class => ServiceCredentialManagerFactory::class, Tinymce::class => TinymceFactory::class, TinymcePageBinder::class => TinymcePageBinderFactory::class, EventDispatcherInterface::class => [EventDispatcherFactory::class, 'create'], diff --git a/src/Service/AcquisitionAction.php b/src/Service/AcquisitionAction.php new file mode 100644 index 00000000..9df58b12 --- /dev/null +++ b/src/Service/AcquisitionAction.php @@ -0,0 +1,18 @@ + '...', 'password' => '...'] + */ + public function asStructured(): array; + + /** + * Returns credential as opaque string. + * For bearer tokens, API keys, or encoded strings. + */ + public function asOpaque(): string; + + /** + * Convenience method for HTTP Bearer header value. + */ + public function asBearerToken(): string; + + /** + * Convenience method for HTTP Basic Auth header value. + * Returns base64-encoded "username:password" + */ + public function asBasicAuth(): string; + + public function createdAt(): int; + public function updatedAt(): int; +} diff --git a/src/Service/PasswordCredentialNotFoundException.php b/src/Service/PasswordCredentialNotFoundException.php new file mode 100644 index 00000000..d06d64f0 --- /dev/null +++ b/src/Service/PasswordCredentialNotFoundException.php @@ -0,0 +1,47 @@ +identifier() + ); + } + parent::__construct($message, $code, $previous); + } + + public function getUserId(): string + { + return $this->userId; + } + + public function getProviderId(): string + { + return $this->providerId; + } + + public function getPurpose(): ServicePurpose + { + return $this->purpose; + } +} diff --git a/src/Service/PasswordServiceAuthorizationService.php b/src/Service/PasswordServiceAuthorizationService.php new file mode 100644 index 00000000..be18accf --- /dev/null +++ b/src/Service/PasswordServiceAuthorizationService.php @@ -0,0 +1,72 @@ +type === CredentialType::OAuth + ? $this->credential->getAccessToken() + : null; + } + + /** + * Convenience accessor for password credential. + */ + public function asPassword(): ?PasswordCredential + { + return $this->type === CredentialType::Password + ? $this->credential + : null; + } +}