Skip to content

Commit

Permalink
Make token endpoint available as Symfony route
Browse files Browse the repository at this point in the history
  • Loading branch information
cicnavi committed Jun 10, 2024
1 parent 21cd0b3 commit f7afedc
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 77 deletions.
8 changes: 7 additions & 1 deletion routing/routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use SimpleSAML\Module\oidc\Codebooks\HttpMethodsEnum;
use SimpleSAML\Module\oidc\Codebooks\RoutesEnum;
use SimpleSAML\Module\oidc\Controller\AccessTokenController;
use SimpleSAML\Module\oidc\Controller\AuthorizationController;
use SimpleSAML\Module\oidc\Controller\ConfigurationDiscoveryController;
use SimpleSAML\Module\oidc\Controller\Federation\EntityStatementController;
Expand All @@ -18,8 +19,13 @@
$routes->add(RoutesEnum::OpenIdConfiguration->name, RoutesEnum::OpenIdConfiguration->value)
->controller(ConfigurationDiscoveryController::class);

/**
* OpenID Connect Core protocol routes.
*/
$routes->add(RoutesEnum::OpenIdAuthorization->name, RoutesEnum::OpenIdAuthorization->value)
->controller([AuthorizationController::class, 'authorize']);
->controller([AuthorizationController::class, 'authorization']);
$routes->add(RoutesEnum::OpenIdToken->name, RoutesEnum::OpenIdToken->value)
->controller([AccessTokenController::class, 'token']);

/**
* OpenID Federation related routes.
Expand Down
1 change: 1 addition & 0 deletions src/Codebooks/RoutesEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum RoutesEnum: string
case OpenIdConfiguration = '.well-known/openid-configuration';
case OpenIdFederationConfiguration = '.well-known/openid-federation';
case OpenIdFederationFetch = 'federation/fetch';
case OpenIdToken = 'token';
}
32 changes: 28 additions & 4 deletions src/Controller/AccessTokenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
*/
namespace SimpleSAML\Module\oidc\Controller;

use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
use SimpleSAML\Module\oidc\Controller\Traits\RequestTrait;
use SimpleSAML\Module\oidc\Repositories\AllowedOriginRepository;
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
use SimpleSAML\Module\oidc\Services\ErrorResponder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class AccessTokenController
{
Expand All @@ -29,19 +33,39 @@ class AccessTokenController
public function __construct(
private readonly AuthorizationServer $authorizationServer,
private readonly AllowedOriginRepository $allowedOriginRepository,
private readonly PsrHttpBridge $psrHttpBridge,
private readonly ErrorResponder $errorResponder,
) {
}

/**
* @throws \League\OAuth2\Server\Exception\OAuthServerException
*/
public function __invoke(ServerRequest $request): ResponseInterface
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
// Check if this is actually a CORS preflight request...
if (strtoupper($request->getMethod()) === 'OPTIONS') {
return $this->handleCors($request);
}

return $this->authorizationServer->respondToAccessTokenRequest($request, new Response());
return $this->authorizationServer->respondToAccessTokenRequest(
$request,
$this->psrHttpBridge->getResponseFactory()->createResponse(),
);
}

public function token(Request $request): Response
{
try {
/**
* @psalm-suppress DeprecatedMethod Until we drop support for old public/*.php routes, we need to bridge
* between PSR and Symfony HTTP messages.
*/
return $this->psrHttpBridge->getHttpFoundationFactory()->createResponse(
$this->__invoke($this->psrHttpBridge->getPsrHttpFactory()->createRequest($request)),
);
} catch (OAuthServerException $exception) {
return $this->errorResponder->forException($exception);
}
}
}
4 changes: 2 additions & 2 deletions src/Controller/AuthorizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function __construct(
* @throws \Throwable
*
* @deprecated 7.0.0 Will be moved to Symfony controller method
* @see self::authorize()
* @see self::authorization()
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
Expand All @@ -77,7 +77,7 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface
);
}

public function authorize(Request $request): Response
public function authorization(Request $request): Response
{
try {
/**
Expand Down
23 changes: 10 additions & 13 deletions src/Controller/Traits/RequestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

namespace SimpleSAML\Module\oidc\Controller\Traits;

use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;

trait RequestTrait
Expand All @@ -28,7 +28,7 @@ trait RequestTrait
*
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
*/
protected function handleCors(ServerRequest $request): Response
protected function handleCors(ServerRequestInterface $request): ResponseInterface
{
$origin = $request->getHeaderLine('Origin');

Expand All @@ -40,15 +40,12 @@ protected function handleCors(ServerRequest $request): Response
throw OidcServerException::accessDenied(sprintf('CORS error: origin %s is not allowed', $origin));
}

$headers = [
'Access-Control-Allow-Origin' => $origin,
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
// Support AJAX requests for JS clients
// e.g. https://github.com/swagger-api/swagger-ui/commit/937c8f6208f3adf713b10a349a82a1b129bd0ffd
'Access-Control-Allow-Headers' => 'Authorization, X-Requested-With',
'Access-Control-Allow-Credentials' => 'true',
];

return new Response('php://memory', 204, $headers);
return $this->psrHttpBridge->getResponseFactory()->createResponse(204)
->withBody($this->psrHttpBridge->getStreamFactory()->createStream('php://memory'))
->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
->withHeader('Access-Control-Allow-Headers', 'Authorization, X-Requested-With')
->withHeader('Access-Control-Allow-Credentials', 'true')
;
}
}
8 changes: 5 additions & 3 deletions src/Controller/UserInfoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

namespace SimpleSAML\Module\oidc\Controller;

use Laminas\Diactoros\Response;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\ServerRequest;
use League\OAuth2\Server\ResourceServer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Error;
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
use SimpleSAML\Module\oidc\Controller\Traits\RequestTrait;
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
use SimpleSAML\Module\oidc\Entities\UserEntity;
Expand All @@ -39,6 +40,7 @@ public function __construct(
private readonly UserRepository $userRepository,
private readonly AllowedOriginRepository $allowedOriginRepository,
private readonly ClaimTranslatorExtractor $claimTranslatorExtractor,
private readonly PsrHttpBridge $psrHttpBridge,
) {
}

Expand All @@ -47,7 +49,7 @@ public function __construct(
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
* @throws \League\OAuth2\Server\Exception\OAuthServerException
*/
public function __invoke(ServerRequest $request): Response
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
// Check if this is actually a CORS preflight request...
if (strtoupper($request->getMethod()) === 'OPTIONS') {
Expand Down
2 changes: 1 addition & 1 deletion src/Services/OpMetadataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private function initMetadata(): void
$this->metadata['issuer'] = $this->moduleConfig->getIssuer();
$this->metadata['authorization_endpoint'] =
$this->moduleConfig->getModuleUrl(RoutesEnum::OpenIdAuthorization->value);
$this->metadata['token_endpoint'] = $this->moduleConfig->getModuleUrl('token.php');
$this->metadata['token_endpoint'] = $this->moduleConfig->getModuleUrl(RoutesEnum::OpenIdToken->value);
$this->metadata['userinfo_endpoint'] = $this->moduleConfig->getModuleUrl('userinfo.php');
$this->metadata['end_session_endpoint'] = $this->moduleConfig->getModuleUrl('logout.php');
$this->metadata['jwks_uri'] = $this->moduleConfig->getModuleUrl('jwks.php');
Expand Down
60 changes: 37 additions & 23 deletions tests/src/Controller/AccessTokenControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
use SimpleSAML\Module\oidc\Controller\AccessTokenController;
use SimpleSAML\Module\oidc\Controller\Traits\RequestTrait;
use SimpleSAML\Module\oidc\Controller\UserInfoController;
use SimpleSAML\Module\oidc\Repositories\AllowedOriginRepository;
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
use SimpleSAML\Module\oidc\Services\ErrorResponder;

/**
* @covers \SimpleSAML\Module\oidc\Controller\AccessTokenController
Expand All @@ -23,6 +26,11 @@ class AccessTokenControllerTest extends TestCase
protected MockObject $allowedOriginRepository;
protected MockObject $serverRequestMock;
protected MockObject $responseMock;
protected MockObject $psrHttpBridgeMock;
protected MockObject $errorResponderMock;
protected MockObject $requestFactoryMock;
protected MockObject $responseFactoryMock;


/**
* @throws \Exception
Expand All @@ -33,16 +41,29 @@ protected function setUp(): void
$this->allowedOriginRepository = $this->createMock(AllowedOriginRepository::class);
$this->serverRequestMock = $this->createMock(ServerRequest::class);
$this->responseMock = $this->createMock(Response::class);
$this->errorResponderMock = $this->createMock(ErrorResponder::class);

$this->psrHttpBridgeMock = $this->createMock(PsrHttpBridge::class);
$this->responseFactoryMock = $this->createMock(ResponseFactoryInterface::class);
$this->responseFactoryMock->method('createResponse')->willReturn($this->responseMock);
$this->psrHttpBridgeMock->method('getResponseFactory')->willReturn($this->responseFactoryMock);
}

protected function mock(): AccessTokenController
{
return new AccessTokenController(
$this->authorizationServerMock,
$this->allowedOriginRepository,
$this->psrHttpBridgeMock,
$this->errorResponderMock,
);
}

public function testItIsInitializable(): void
{
$this->assertInstanceOf(
AccessTokenController::class,
new AccessTokenController(
$this->authorizationServerMock,
$this->allowedOriginRepository,
),
$this->mock(),
);
}

Expand All @@ -54,36 +75,29 @@ public function testItRespondsToAccessTokenRequest(): void
$this->authorizationServerMock
->expects($this->once())
->method('respondToAccessTokenRequest')
->with($this->serverRequestMock, $this->isInstanceOf(Response::class))
->with($this->serverRequestMock, $this->isInstanceOf(ResponseInterface::class))
->willReturn($this->responseMock);

$this->assertSame(
$this->responseMock,
(new AccessTokenController(
$this->authorizationServerMock,
$this->allowedOriginRepository,
))->__invoke($this->serverRequestMock),
$this->mock()->__invoke($this->serverRequestMock),
);
}

public function testItHandlesCorsRequest(): void
{
$this->serverRequestMock->expects($this->once())->method('getMethod')->willReturn('OPTIONS');
$userInfoControllerMock = $this->getMockBuilder(UserInfoController::class)
->disableOriginalConstructor()
->onlyMethods(['handleCors'])
->getMock();
$userInfoControllerMock->expects($this->once())->method('handleCors');
$this->serverRequestMock->expects($this->once())->method('getHeaderLine')->with('Origin')
->willReturn('http://localhost');
$this->allowedOriginRepository->expects($this->once())->method('has')
->with('http://localhost')
->willReturn(true);

$userInfoControllerMock->__invoke($this->serverRequestMock);
}
$this->responseMock->expects($this->atLeast(4))->method('withHeader')
->willReturnSelf();
$this->responseMock->method('withBody')->willReturnSelf();

/**
* @return \SimpleSAML\Module\oidc\Controller\AccessTokenController
*/
protected function prepareMockedInstance(): AccessTokenController
{
return new AccessTokenController($this->authorizationServerMock, $this->allowedOriginRepository);
$this->mock()->__invoke($this->serverRequestMock);
}

public function testItUsesRequestTrait(): void
Expand Down
4 changes: 2 additions & 2 deletions tests/src/Controller/ConfigurationDiscoveryControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class ConfigurationDiscoveryControllerTest extends TestCase
{
final public const OIDC_OP_METADATA = [
'issuer' => 'http://localhost',
'authorization_endpoint' => 'http://localhost/authorize.php',
'token_endpoint' => 'http://localhost/token.php',
'authorization_endpoint' => 'http://localhost/authorization',
'token_endpoint' => 'http://localhost/token',
'userinfo_endpoint' => 'http://localhost/userinfo.php',
'jwks_uri' => 'http://localhost/jwks.php',
'scopes_supported' => ['openid'],
Expand Down
Loading

0 comments on commit f7afedc

Please sign in to comment.