diff --git a/Controller/IntrospectionController.php b/Controller/IntrospectionController.php
index 2c26f681..3e6a1635 100644
--- a/Controller/IntrospectionController.php
+++ b/Controller/IntrospectionController.php
@@ -13,13 +13,19 @@
namespace FOS\OAuthServerBundle\Controller;
+use FOS\OAuthServerBundle\Form\Model\Introspect;
+use FOS\OAuthServerBundle\Form\Type\IntrospectionFormType;
use FOS\OAuthServerBundle\Model\AccessTokenInterface;
use FOS\OAuthServerBundle\Model\RefreshTokenInterface;
use FOS\OAuthServerBundle\Model\TokenInterface;
use FOS\OAuthServerBundle\Model\TokenManagerInterface;
+use FOS\OAuthServerBundle\Security\Authentication\Token\OAuthToken;
+use Symfony\Component\Form\FormFactory;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class IntrospectionController
{
@@ -38,21 +44,47 @@ class IntrospectionController
*/
private $refreshTokenManager;
+ /**
+ * @var FormFactory
+ */
+ private $formFactory;
+
+ /**
+ * @var array
+ */
+ private $allowedIntrospectionClients;
+
public function __construct(
TokenStorageInterface $tokenStorage,
TokenManagerInterface $accessTokenManager,
- TokenManagerInterface $refreshTokenManager
+ TokenManagerInterface $refreshTokenManager,
+ FormFactory $formFactory,
+ array $allowedIntrospectionClients
) {
$this->tokenStorage = $tokenStorage;
$this->accessTokenManager = $accessTokenManager;
$this->refreshTokenManager = $refreshTokenManager;
+ $this->formFactory = $formFactory;
+ $this->allowedIntrospectionClients = $allowedIntrospectionClients;
}
public function introspectAction(Request $request): JsonResponse
{
- // $clientToken = $this->tokenStorage->getToken(); → use in security
+ $clientToken = $this->tokenStorage->getToken(); // → use in security
+
+ if (!$clientToken instanceof OAuthToken) {
+ throw new AccessDeniedException('The introspect endpoint must be behind a secure firewall.');
+ }
- // TODO security for this endpoint. Probably in the README documentation
+ $callerToken = $this->accessTokenManager->findTokenByToken($clientToken->getToken());
+
+ if (!$callerToken) {
+ throw new AccessDeniedException('The access token must have a valid token.');
+ }
+
+ if (!in_array($callerToken->getClientId(), $this->allowedIntrospectionClients)) {
+ throw new AccessDeniedException('This access token is not autorised to do introspection.');
+ }
$token = $this->getToken($request);
@@ -79,8 +111,9 @@ public function introspectAction(Request $request): JsonResponse
*/
private function getToken(Request $request)
{
- $tokenTypeHint = $request->request->get('token_type_hint'); // TODO move in a form type ? can be `access_token`, `refresh_token` See https://tools.ietf.org/html/rfc7009#section-4.1.2
- $tokenString = $request->request->get('token'); // TODO move in a form type ?
+ $formData = $this->processIntrospectionForm($request);
+ $tokenString = $formData->token;
+ $tokenTypeHint = $formData->token_type_hint;
$tokenManagerList = [];
if (!$tokenTypeHint || 'access_token' === $tokenTypeHint) {
@@ -125,4 +158,21 @@ private function getUsername(TokenInterface $token)
return $user->getUserName();
}
+
+ private function processIntrospectionForm(Request $request): Introspect
+ {
+ $formData = new Introspect();
+ $form = $this->formFactory->create(IntrospectionFormType::class, $formData);
+ $form->handleRequest($request);
+
+ if (!$form->isSubmitted() || !$form->isValid()) {
+ $errors = $form->getErrors();
+ if (count($errors) > 0) {
+ throw new BadRequestHttpException((string) $errors);
+ } else {
+ throw new BadRequestHttpException('Introspection endpoint needs to have at least a "token" form parameter');
+ }
+ }
+ return $form->getData();
+ }
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 571fcafd..d2500fea 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -82,6 +82,7 @@ public function getConfigTreeBuilder()
$this->addAuthorizeSection($rootNode);
$this->addServiceSection($rootNode);
$this->addTemplateSection($rootNode);
+ $this->addIntrospectionSection($rootNode);
return $treeBuilder;
}
@@ -151,4 +152,24 @@ private function addTemplateSection(ArrayNodeDefinition $node)
->end()
;
}
+
+ private function addIntrospectionSection(ArrayNodeDefinition $node)
+ {
+ $node
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->arrayNode('introspection')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->arrayNode('allowed_clients')
+ ->useAttributeAsKey('key')
+ ->treatNullLike([])
+ ->prototype('variable')->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
}
diff --git a/DependencyInjection/FOSOAuthServerExtension.php b/DependencyInjection/FOSOAuthServerExtension.php
index 1a1fc008..8b84433a 100644
--- a/DependencyInjection/FOSOAuthServerExtension.php
+++ b/DependencyInjection/FOSOAuthServerExtension.php
@@ -102,7 +102,7 @@ public function load(array $configs, ContainerBuilder $container)
$authorizeFormDefinition->setFactory([new Reference('form.factory'), 'createNamed']);
}
- $this->loadIntrospection($loader);
+ $this->loadIntrospection($config, $container, $loader);
}
/**
@@ -144,9 +144,12 @@ protected function remapParametersNamespaces(array $config, ContainerBuilder $co
}
}
- protected function loadIntrospection(XmlFileLoader $loader)
+ protected function loadIntrospection(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
$loader->load('introspection.xml');
+
+ $allowedClients = $config['introspection']['allowed_clients'];
+ $container->setParameter('fos_oauth_server.introspection.allowed_clients', $allowedClients);
}
protected function loadAuthorize(array $config, ContainerBuilder $container, XmlFileLoader $loader)
diff --git a/Form/Model/Introspect.php b/Form/Model/Introspect.php
new file mode 100644
index 00000000..b419ed6a
--- /dev/null
+++ b/Form/Model/Introspect.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\OAuthServerBundle\Form\Model;
+
+use Symfony\Component\Validator\Constraints as Assert;
+
+class Introspect
+{
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public $token;
+
+ /**
+ * @var string
+ */
+ public $token_type_hint;
+}
diff --git a/Form/Type/IntrospectionFormType.php b/Form/Type/IntrospectionFormType.php
new file mode 100644
index 00000000..b169cf5a
--- /dev/null
+++ b/Form/Type/IntrospectionFormType.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\OAuthServerBundle\Form\Type;
+
+use FOS\OAuthServerBundle\Form\Model\Introspect;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class IntrospectionFormType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->add('token', HiddenType::class);
+ $builder->add('token_type_hint', ChoiceType::class, [ // can be `access_token`, `refresh_token` See https://tools.ietf.org/html/rfc7009#section-4.1.2
+ 'choices' => [
+ 'access_token' => 'access_token',
+ 'refresh_token' => 'refresh_token',
+ ]
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'data_class' => Introspect::class,
+ 'csrf_protection' => false,
+ ]);
+ }
+
+ /**
+ * @return string
+ */
+ public function getBlockPrefix()
+ {
+ return '';
+ }
+}
diff --git a/Resources/config/introspection.xml b/Resources/config/introspection.xml
index 7bc972b1..da9d1cd6 100644
--- a/Resources/config/introspection.xml
+++ b/Resources/config/introspection.xml
@@ -9,6 +9,8 @@
+
+ %fos_oauth_server.introspection.allowed_clients%