-
-
Notifications
You must be signed in to change notification settings - Fork 798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Guidance on runtime dynamic Resource Owner configuration #1959
Comments
Message to comment on stale issues. If none provided, will not mark issues stale |
Have you found anything about this? I would like to set the configuration in the database as well. I've got a use case where I have one codebase with different domains that need different configs. |
Yes, I found a way, it's not super elegant but it works: I added an injection pass in the Kernel to pass the keys dynamically: class OAuthResourceServersInjectionPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$this->setupContainer($container, 'hwi_oauth.resource_owner.github', 'users.resource_server.github.client_id', 'users.resource_server.github.client_secret');
}
private function setupContainer(ContainerBuilder $container, string $resourceServerIdentifier, string $clientIdConfigurationKey, string $clientSecretConfigurationKey)
{
if ($container->has($resourceServerIdentifier)) {
$definition = $container->findDefinition($resourceServerIdentifier);
$definition->addMethodCall('setEntityManager', [new Reference('doctrine.orm.entity_manager')]);
$definition->addMethodCall('setClientIdConfigurationKey', [$clientIdConfigurationKey]);
$definition->addMethodCall('setClientSecretConfigurationKey', [$clientSecretConfigurationKey]);
}
}
} Then I had to recreate (mostly copypaste and tweak) the "ResourceOwner" classes to accept dynamic values:
Hope that helps? |
Looking for the same. Do you have the full code that we can review? |
That's what I have so far, verbatim from my project. I'll let you remove what isn't relevant to your use case and filter out my stuff (My project is called Spectram). Side note: my understanding of OAuth is that the resource owner is the actual human (the end user), and the service they're using to connect is the resource server. I have reflected that in the naming of my classes (e.g. Below is the relevant code. Hope this helps.
<?php
namespace App;
use App\Service\OAuthResourceServer\OAuthResourceServersInjectionPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new OAuthResourceServersInjectionPass());
}
}
<?php
namespace App\Service\OAuthResourceServer;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class OAuthResourceServersInjectionPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$this->setupContainer($container, 'hwi_oauth.resource_owner.github', 'users.resource_server.github.client_id', 'users.resource_server.github.client_secret');
$this->setupContainer($container, 'hwi_oauth.resource_owner.google', 'users.resource_server.google.client_id', 'users.resource_server.google.client_secret');
}
private function setupContainer(ContainerBuilder $container, string $resourceServerIdentifier, string $clientIdConfigurationKey, string $clientSecretConfigurationKey)
{
if ($container->has($resourceServerIdentifier)) {
$definition = $container->findDefinition($resourceServerIdentifier);
$definition->addMethodCall('setEntityManager', [new Reference('doctrine.orm.entity_manager')]);
$definition->addMethodCall('setClientIdConfigurationKey', [$clientIdConfigurationKey]);
$definition->addMethodCall('setClientSecretConfigurationKey', [$clientSecretConfigurationKey]);
}
}
}
<?php
namespace App\Service\OAuthResourceServer;
use App\Entity\SpectramConfiguration;
use Doctrine\ORM\EntityManagerInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use HWI\Bundle\OAuthBundle\Security\Helper\NonceGenerator;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Contracts\HttpClient\ResponseInterface;
abstract class DynamicOAuth2ResourceServer extends GenericOAuth2ResourceOwner
{
public const TYPE = null; // it must be null
private EntityManagerInterface $entityManager;
protected string $clientIdConfigurationKey = '';
protected string $clientSecretConfigurationKey = '';
public function setEntityManager(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function setClientIdConfigurationKey(string $clientIdConfigurationKey)
{
$this->clientIdConfigurationKey = $clientIdConfigurationKey;
}
public function setClientSecretConfigurationKey(string $clientSecretConfigurationKey)
{
$this->clientSecretConfigurationKey = $clientSecretConfigurationKey;
}
protected function getClientId(): string {
return $this->entityManager
->getRepository(SpectramConfiguration::class)
->findOneByName($this->clientIdConfigurationKey)
->getStringValue()
;
}
protected function getClientSecret(): string {
return $this->entityManager
->getRepository(SpectramConfiguration::class)
->findOneByName($this->clientSecretConfigurationKey)
->getStringValue()
;
}
/**
* {@inheritdoc}
*/
public function getAuthorizationUrl($redirectUri, array $extraParameters = []): string
{
if ($this->options['csrf']) {
$this->handleCsrfToken();
}
$parameters = array_merge([
'response_type' => 'code',
'client_id' => $this->getClientId(),
'scope' => $this->options['scope'],
'state' => $this->state->encode(),
'redirect_uri' => $redirectUri,
], $extraParameters);
return $this->normalizeUrl($this->options['authorization_url'], $parameters);
}
/**
* {@inheritdoc}
*/
public function revokeToken($token): bool
{
if (!isset($this->options['revoke_token_url'])) {
throw new AuthenticationException('OAuth error: "Method unsupported."');
}
$parameters = [
'client_id' => $this->getClientId(),
'client_secret' => $this->getClientSecret(),
];
$response = $this->httpRequest($this->normalizeUrl($this->options['revoke_token_url'], ['token' => $token]), $parameters, [], 'DELETE');
return 200 === $response->getStatusCode();
}
/**
* {@inheritdoc}
*/
protected function doGetTokenRequest($url, array $parameters = []): ResponseInterface
{
$headers = [];
if ($this->options['use_authorization_to_get_token']) {
if ($this->getClientSecret()) {
$headers['Authorization'] = 'Basic '.base64_encode($this->getClientId().':'.$this->getClientSecret());
}
} else {
$parameters['client_id'] = $this->getClientId();
$parameters['client_secret'] = $this->getClientSecret();
}
return $this->httpRequest($url, http_build_query($parameters, '', '&'), $headers);
}
private function handleCsrfToken(): void
{
if (null === $this->state->getCsrfToken()) {
$this->state->setCsrfToken(NonceGenerator::generate());
}
$this->storage->save($this, $this->state->getCsrfToken(), 'csrf_state');
}
}
<?php
namespace App\Service\OAuthResourceServer;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class GitHubResourceServer extends DynamicOAuth2ResourceServer
{
public const TYPE = 'github';
protected array $paths = [
'identifier' => 'id',
'nickname' => 'login',
'realname' => 'name',
'email' => 'email',
'profilepicture' => 'avatar_url',
];
public function getUserInformation(array $accessToken, array $extraParameters = []): UserResponseInterface
{
$response = parent::getUserInformation($accessToken, $extraParameters);
$responseData = $response->getData();
if (empty($responseData['email'])) {
// fetch the email addresses linked to the account
$content = $this->httpRequest(
$this->normalizeUrl($this->options['emails_url']), null, ['Authorization' => 'Bearer '.$accessToken['access_token']]
);
foreach ($this->getResponseContent($content) as $email) {
if (!empty($email['primary'])) {
// we only need the primary email address
$responseData['email'] = $email['email'];
break;
}
}
$response->setData($responseData);
}
return $response;
}
public function revokeToken($token): bool
{
$response = $this->httpRequest(
sprintf($this->options['revoke_token_url'], parent::getClientId()),
json_encode(['access_token' => $token]),
[
'Authorization' => 'Basic '.base64_encode(parent::getClientId().':'.parent::getClientSecret()),
'Content-Type' => 'application/json',
],
'DELETE'
);
return 204 === $response->getStatusCode();
}
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'authorization_url' => 'https://github.com/login/oauth/authorize',
'access_token_url' => 'https://github.com/login/oauth/access_token',
'revoke_token_url' => 'https://api.github.com/applications/%s/token',
'infos_url' => 'https://api.github.com/user',
'emails_url' => 'https://api.github.com/user/emails',
'use_commas_in_scope' => true,
]);
}
} |
Hi 👋
I'm working on a Symfony
6.3
project using HWIOAuthBundle2.0.0
, and I'm looking for guidance on making resource owner configurations dynamic and updatable at runtime.I want to allow specific users ("Admin" users) to set the
client_id
andclient_secret
for built-in resource owners (e.g., GitHub, BitBucket, GitLab, LinkedIn). Theclient_id
andclient_secret
pairs are stored in a database and will be updated infrequently, making them suitable for heavy caching.At the moment I'm toying with a Compiler Pass that updates the definition for each resource owners, as necessary. This feels a bit over-engineered, and I don't know how to tell the service container to recompile the service when the values have been updated?
What's the best or simplest way to achieve this? Is this even a supported use case, or will I have to find a way around it for the time being?
Thanks for your help, and thanks for publishing this bundle 🙏 !
The text was updated successfully, but these errors were encountered: