Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

POC Introspection endpoint RFC 7662 #590

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions Controller/IntrospectionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

/*
* This file is part of the FOSOAuthServerBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

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
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;

/**
* @var TokenManagerInterface
*/
private $accessTokenManager;

/**
* @var TokenManagerInterface
*/
private $refreshTokenManager;

/**
* @var FormFactory
*/
private $formFactory;

/**
* @var array
*/
private $allowedIntrospectionClients;

public function __construct(
TokenStorageInterface $tokenStorage,
TokenManagerInterface $accessTokenManager,
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
{
$this->denyAccessIfNotAuthorizedClient();

$token = $this->getToken($request);

$isActive = $token && !$token->hasExpired();

if (!$isActive) {
return new JsonResponse([
'active' => false,
]);
}

return new JsonResponse([
'active' => true,
'scope' => $token->getScope(),
'client_id' => $token->getClientId(),
'username' => $this->getUsername($token),
'token_type' => $this->getTokenType($token),
'exp' => $token->getExpiresAt(),
]);
}

/**
* Check that the caller has a token generated by an allowed client
*/
private function denyAccessIfNotAuthorizedClient(): void
{
$clientToken = $this->tokenStorage->getToken();

if (!$clientToken instanceof OAuthToken) {
throw new AccessDeniedException('The introspect endpoint must be behind a secure firewall.');
}

$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.');
}
}

/**
* @return TokenInterface|null
*/
private function getToken(Request $request)
{
$formData = $this->processIntrospectionForm($request);
$tokenString = $formData->token;
$tokenTypeHint = $formData->token_type_hint;

$tokenManagerList = [];
if (!$tokenTypeHint || 'access_token' === $tokenTypeHint) {
$tokenManagerList[] = $this->accessTokenManager;
}
if (!$tokenTypeHint || 'refresh_token' === $tokenTypeHint) {
$tokenManagerList[] = $this->refreshTokenManager;
}

foreach ($tokenManagerList as $tokenManager) {
$token = $tokenManager->findTokenByToken($tokenString);

if ($token) {
return $token;
}
}
}

/**
* @return string|null
*/
private function getTokenType(TokenInterface $token)
{
if ($token instanceof AccessTokenInterface) {
return 'access_token';
} elseif ($token instanceof RefreshTokenInterface) {
return 'refresh_token';
}

return null;
}

/**
* @return string|null
*/
private function getUsername(TokenInterface $token)
{
$user = $token->getUser();
if (!$user) {
return null;
}

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();
}
}
36 changes: 36 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public function getConfigTreeBuilder()

$this->addAuthorizeSection($rootNode);
$this->addServiceSection($rootNode);
$this->addTemplateSection($rootNode);
$this->addIntrospectionSection($rootNode);

return $treeBuilder;
}
Expand Down Expand Up @@ -134,4 +136,38 @@ private function addServiceSection(ArrayNodeDefinition $node)
->end()
;
}

private function addTemplateSection(ArrayNodeDefinition $node)
{
$node
->children()
->arrayNode('template')
->addDefaultsIfNotSet()
->children()
->scalarNode('engine')->defaultValue('twig')->end()
->end()
->end()
->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()
;
}
}
10 changes: 10 additions & 0 deletions DependencyInjection/FOSOAuthServerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public function load(array $configs, ContainerBuilder $container)
$authorizeFormDefinition = $container->getDefinition('fos_oauth_server.authorize.form');
$authorizeFormDefinition->setFactory([new Reference('form.factory'), 'createNamed']);
}

$this->loadIntrospection($config, $container, $loader);
}

/**
Expand Down Expand Up @@ -140,6 +142,14 @@ protected function remapParametersNamespaces(array $config, ContainerBuilder $co
}
}

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)
{
$loader->load('authorize.xml');
Expand Down
30 changes: 30 additions & 0 deletions Form/Model/Introspect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/*
* This file is part of the FOSOAuthServerBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* 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;
}
54 changes: 54 additions & 0 deletions Form/Type/IntrospectionFormType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/*
* This file is part of the FOSOAuthServerBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* 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 '';
}
}
17 changes: 17 additions & 0 deletions Resources/config/introspection.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="fos_oauth_server.controller.introspection" class="FOS\OAuthServerBundle\Controller\IntrospectionController" public="true">
<argument type="service" id="security.token_storage" />
<argument type="service" id="fos_oauth_server.access_token_manager" />
<argument type="service" id="fos_oauth_server.refresh_token_manager" />
<argument type="service" id="form.factory" />
<argument>%fos_oauth_server.introspection.allowed_clients%</argument>
</service>
</services>

</container>
12 changes: 12 additions & 0 deletions Resources/config/routing/introspection.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="fos_oauth_server_introspection" path="/oauth/v2/introspect" methods="POST">
<default key="_controller">fos_oauth_server.controller.introspection:introspectAction</default>
</route>

</routes>

2 changes: 2 additions & 0 deletions Resources/doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,5 @@ The `authorize` endpoint is at `/oauth/v2/auth` by default (see `Resources/confi
[Adding Grant Extensions](adding_grant_extensions.md)

[Custom DB Driver](custom_db_driver.md)

[Introspection endpoint](introspection_endpoint.md)
Loading