Commit 0e532d0c authored by Oliver Bartsch's avatar Oliver Bartsch

Replace OAuth2 with Bearer Auth

parent ee053a0f
Pipeline #9799 failed with stages
in 3 minutes and 5 seconds
#!/bin/bash
## Description: Generate ecdsa keys for generating tokens in the REST API
## Usage: create-keys
## Example: "ddev create-keys"
readonly KEYS_PATH=/var/www/html
mkdir -p $KEYS_PATH
openssl ecparam -name secp521r1 -genkey -noout -out $KEYS_PATH/private.test.ec.key
openssl pkcs8 -topk8 -in $KEYS_PATH/private.test.ec.key -out $KEYS_PATH/private.test.pem -passin env:ECDSA_KEY_PASSPHRASE -passout env:ECDSA_KEY_PASSPHRASE
openssl ec -in $KEYS_PATH/private.test.pem -pubout -out $KEYS_PATH/public.test.pem -passin env:ECDSA_KEY_PASSPHRASE
rm $KEYS_PATH/private.test.ec.key
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Entity;
namespace T3o\TerRest\Authentication\Bearer\Entity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......
......@@ -10,20 +10,20 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Entity;
namespace T3o\TerRest\Authentication\Bearer\Entity;
/**
* OAuth2 access token entity
* Access token entity
*/
class AccessToken extends AbstractToken
{
protected int $client;
protected int $user;
protected int $scope;
protected int $updated;
public function __construct(
string $id,
int $client,
int $user,
int $created,
int $expires,
int $scope,
......@@ -31,14 +31,14 @@ class AccessToken extends AbstractToken
int $updated = 0
) {
parent::__construct($id, $created, $expires, $revoked);
$this->client = $client;
$this->user = $user;
$this->scope = $scope;
$this->updated = $updated;
}
public function getClient(): int
public function getUser(): int
{
return $this->client;
return $this->user;
}
public function getScope(): int
......
......@@ -10,10 +10,10 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Entity;
namespace T3o\TerRest\Authentication\Bearer\Entity;
/**
* OAuth2 refresh token entity
* Refresh token entity
*/
class RefreshToken extends AbstractToken
{
......
......@@ -10,10 +10,10 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Entity;
namespace T3o\TerRest\Authentication\Bearer\Entity;
/**
* Interface to be implemented by OAuth2 token entities
* Interface to be implemented by token entities
*/
interface TokenInterface
{
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Lcobucci\JWT\Parser;
use Psr\Http\Message\ServerRequestInterface;
......@@ -20,12 +20,12 @@ use T3o\TerRest\Exception\InvalidRequestedScopeException;
use T3o\TerRest\Exception\TokenIssueException;
use T3o\TerRest\Exception\TokenMissingException;
use T3o\TerRest\Http\ResponseFactory;
use T3o\TerRest\OAuth2\Entity\AccessToken;
use T3o\TerRest\OAuth2\Entity\RefreshToken;
use T3o\TerRest\OAuth2\JwtTokenGenerator;
use T3o\TerRest\OAuth2\JwtTokenValidator;
use T3o\TerRest\OAuth2\Repository\AccessTokenRepository;
use T3o\TerRest\OAuth2\Repository\RefreshTokenRepository;
use T3o\TerRest\Authentication\Bearer\Entity\AccessToken;
use T3o\TerRest\Authentication\Bearer\Entity\RefreshToken;
use T3o\TerRest\Authentication\Jwt\TokenGenerator;
use T3o\TerRest\Authentication\Jwt\TokenValidator;
use T3o\TerRest\Authentication\Bearer\Repository\AccessTokenRepository;
use T3o\TerRest\Authentication\Bearer\Repository\RefreshTokenRepository;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Crypto\Random;
......@@ -52,8 +52,8 @@ abstract class AbstractGrant implements GrantInterface
protected Random $random;
protected AccessTokenRepository $accessTokenRepository;
protected RefreshTokenRepository $refreshTokenRepository;
protected JwtTokenGenerator $jwtTokenGenerator;
protected JwtTokenValidator $jwtTokenValidator;
protected TokenGenerator $jwtTokenGenerator;
protected TokenValidator $jwtTokenValidator;
protected int $timestamp;
protected int $expires;
......@@ -63,8 +63,8 @@ abstract class AbstractGrant implements GrantInterface
Random $random,
AccessTokenRepository $accessTokenRepository,
RefreshTokenRepository $refreshTokenRepository,
JwtTokenGenerator $jwtTokenGenerator,
JwtTokenValidator $jwtTokenValidator
TokenGenerator $jwtTokenGenerator,
TokenValidator $jwtTokenValidator
) {
$this->responseFactory = $responseFactory;
$this->context = $context;
......@@ -82,11 +82,11 @@ abstract class AbstractGrant implements GrantInterface
return $this->identifier;
}
protected function issueAccessToken(ApiUserInterface $client, int $scope): AccessToken
protected function issueAccessToken(ApiUserInterface $user, int $scope): AccessToken
{
$accessToken = new AccessToken(
$this->random->generateRandomHexString(static::RANDOM_LENGTH),
$client->getId(),
$user->getId(),
$this->timestamp,
$this->expires,
$scope
......@@ -121,23 +121,24 @@ abstract class AbstractGrant implements GrantInterface
if ($token === '') {
throw new TokenMissingException(
'Request is missing the \'token\' for grant type: ' . $this->identifier . ' in the request body.',
'Ensure the \'token\' for grant type \'' . $this->identifier . '\' is set in the request body
using Content-Type: \'application/x-www-form-urlencoded\'.',
1603113504
);
}
$jwtToken = (new Parser())->parse($token);
$platform = (string)$request->getAttribute('site')->getBase();
$client = $request->getAttribute('api.user');
$user = $request->getAttribute('api.user');
$this->jwtTokenValidator->validate($jwtToken, $platform, $client, $tokenType);
$this->jwtTokenValidator->validate($jwtToken, $platform, $user, $tokenType);
return [$jwtToken, $platform, $client];
return [$jwtToken, $platform, $user];
}
protected function getScope(
ServerRequestInterface $request,
ApiUserInterface $client,
ApiUserInterface $user,
int $default = Scope::EXTENSION_NONE
): int {
$requestedScope = $request->getAttribute('routing')->getRouteArguments()['scope'] ?? '';
......@@ -148,7 +149,7 @@ abstract class AbstractGrant implements GrantInterface
$scope = Scope::convert($requestedScope->getValue());
if ($scope >= Scope::EXTENSION_ADMIN && !$client->getScope()->isGranted(Scope::EXTENSION_ADMIN)) {
if ($scope >= Scope::EXTENSION_ADMIN && !$user->getScope()->isGranted(Scope::EXTENSION_ADMIN)) {
throw new InvalidRequestedScopeException(
'Scope: extension:admin is not allowed for this user!',
1603107953
......
......@@ -10,9 +10,10 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Create grant types
......@@ -50,6 +51,11 @@ class GrantFactory
return $this->availableGrants[$type];
}
public function createFromOperationId(string $operationId): GrantInterface
{
return $this->create(GeneralUtility::camelCaseToLowerCaseUnderscored($operationId));
}
public function hasGrantType(string $type): bool
{
return isset($this->availableGrants[$type]);
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Doctrine\DBAL\Driver\Exception;
use Psr\Http\Message\ResponseInterface;
......@@ -23,16 +23,16 @@ use T3o\TerRest\Exception\TokenIssueException;
* Implementation for OAuth2 client credentials grant.
* This grant issues access and refresh tokens if the user is allowed to do so.
*/
class ClientCredentialsGrant extends AbstractGrant
class IssueTokenGrant extends AbstractGrant
{
protected string $identifier = 'client_credentials';
protected string $identifier = 'issue_token';
public function handle(ServerRequestInterface $request): ResponseInterface
{
$client = $request->getAttribute('api.user');
$user = $request->getAttribute('api.user');
try {
$accessToken = $this->issueAccessToken($client, $this->getScope($request, $client));
$accessToken = $this->issueAccessToken($user, $this->getScope($request, $user));
$refreshToken = $this->issueRefreshToken($accessToken);
} catch (InvalidRequestedScopeException $e) {
return $this->responseFactory->createUnauthorizedResponse($request, $e->getCode(), $e->getError(), $e->getMessage());
......@@ -46,7 +46,7 @@ class ClientCredentialsGrant extends AbstractGrant
return $this->responseFactory->createTokenCreatedResponse(
$request,
$this->jwtTokenGenerator->generate($platform, $accessToken, ['client' => $client->getId()]),
$this->jwtTokenGenerator->generate($platform, $accessToken, ['user' => $user->getId()]),
$this->jwtTokenGenerator->generate($platform, $refreshToken, ['issued_for' => $accessToken->getId()]),
$this->timestamp + self::LIFETIME,
(string)(new Scope($accessToken->getScope()))
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Doctrine\DBAL\Driver\Exception;
use Psr\Http\Message\ResponseInterface;
......@@ -30,7 +30,7 @@ class RefreshTokenGrant extends AbstractGrant
public function handle(ServerRequestInterface $request): ResponseInterface
{
try {
[$jwtToken, $platform, $client] = $this->initialize($request, 'refresh_token');
[$jwtToken, $platform, $user] = $this->initialize($request, 'refresh_token');
$accessToken = $this->accessTokenRepository->findById((string)$jwtToken->getClaim('issued_for'));
$accessToken->setUpdated($this->timestamp)->setExpires($this->expires);
$refreshToken = $this->refreshTokenRepository->findById((string)$jwtToken->getClaim('jti'));
......@@ -47,7 +47,7 @@ class RefreshTokenGrant extends AbstractGrant
return $this->responseFactory->createTokenCreatedResponse(
$request,
$this->jwtTokenGenerator->generate($platform, $accessToken, ['client' => $client->getId()]),
$this->jwtTokenGenerator->generate($platform, $accessToken, ['user' => $user->getId()]),
$this->jwtTokenGenerator->generate($platform, $refreshToken, ['issued_for' => $accessToken->getId()]),
$this->expires,
(string)(new Scope($accessToken->getScope()))
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Grant;
namespace T3o\TerRest\Authentication\Bearer\Grant;
use Doctrine\DBAL\Driver\Exception;
use Psr\Http\Message\ResponseInterface;
......
......@@ -2,7 +2,7 @@
declare(strict_types = 1);
namespace T3o\TerRest\OAuth2\Repository;
namespace T3o\TerRest\Authentication\Bearer\Repository;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
......@@ -12,7 +12,7 @@ namespace T3o\TerRest\OAuth2\Repository;
* of the License, or any later version.
*/
use T3o\TerRest\OAuth2\Entity\TokenInterface;
use T3o\TerRest\Authentication\Bearer\Entity\TokenInterface;
use T3o\TerRest\Repository\AbstractRepository;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......
......@@ -2,7 +2,7 @@
declare(strict_types = 1);
namespace T3o\TerRest\OAuth2\Repository;
namespace T3o\TerRest\Authentication\Bearer\Repository;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
......@@ -13,7 +13,7 @@ namespace T3o\TerRest\OAuth2\Repository;
*/
/**
* Repository for OAuth2 access tokens
* Repository for access tokens
*/
class AccessTokenRepository extends AbstractTokenRepository
{
......
......@@ -2,7 +2,7 @@
declare(strict_types = 1);
namespace T3o\TerRest\OAuth2\Repository;
namespace T3o\TerRest\Authentication\Bearer\Repository;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
......@@ -12,10 +12,10 @@ namespace T3o\TerRest\OAuth2\Repository;
* of the License, or any later version.
*/
use T3o\TerRest\OAuth2\Entity\RefreshToken;
use T3o\TerRest\Authentication\Bearer\Entity\RefreshToken;
/**
* Repository for OAuth2 refresh tokens
* Repository for refresh tokens
*/
class RefreshTokenRepository extends AbstractTokenRepository
{
......
......@@ -2,7 +2,7 @@
declare(strict_types = 1);
namespace T3o\TerRest\OAuth2\Repository;
namespace T3o\TerRest\Authentication\Bearer\Repository;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
......@@ -12,7 +12,7 @@ namespace T3o\TerRest\OAuth2\Repository;
* of the License, or any later version.
*/
use T3o\TerRest\OAuth2\Entity\TokenInterface;
use T3o\TerRest\Authentication\Bearer\Entity\TokenInterface;
/**
* Interface to be implemented by token repositories
......
......@@ -10,7 +10,7 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2\Repository;
namespace T3o\TerRest\Authentication\Bearer\Repository;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
......
......@@ -10,17 +10,17 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2;
namespace T3o\TerRest\Authentication\Jwt;
use Lcobucci\JWT\Builder;
use T3o\TerRest\Crypto\EcdsaSignature;
use T3o\TerRest\Crypto\SignatureFactory;
use T3o\TerRest\OAuth2\Entity\TokenInterface;
use T3o\TerRest\Authentication\Bearer\Entity\TokenInterface;
/**
* Generation of JWT tokens for OAuth2 implementation
* Generation of JWT tokens for bearerAuth implementation
*/
final class JwtTokenGenerator
final class TokenGenerator
{
public const JWT_SUB = 't3o-ter-rest';
private const JWT_LATENCY = 60;
......
......@@ -10,15 +10,15 @@ declare(strict_types = 1);
* of the License, or any later version.
*/
namespace T3o\TerRest\OAuth2;
namespace T3o\TerRest\Authentication\Jwt;
use Lcobucci\JWT\Token;
use T3o\TerRest\Authentication\User\ApiUserInterface;
use T3o\TerRest\Crypto\EcdsaSignature;
use T3o\TerRest\Crypto\SignatureFactory;
use T3o\TerRest\Exception\InvalidJwtTokenException;
use T3o\TerRest\OAuth2\Repository\AccessTokenRepository;
use T3o\TerRest\OAuth2\Repository\RefreshTokenRepository;
use T3o\TerRest\Authentication\Bearer\Repository\AccessTokenRepository;
use T3o\TerRest\Authentication\Bearer\Repository\RefreshTokenRepository;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -38,7 +38,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*
* Note: This validator will also always check the tokens counterpart.
*/
final class JwtTokenValidator
final class TokenValidator
{
protected SignatureFactory $signatureFactory;
protected AccessTokenRepository $accessTokenRepository;
......@@ -57,7 +57,7 @@ final class JwtTokenValidator
public function validate(
Token $token,
string $platform = '',
ApiUserInterface $client = null,
ApiUserInterface $user = null,
string $tokenType = 'access_token',
string $signatureIdentifier = EcdsaSignature::ECDSA_IDENTIDIER
): bool {
......@@ -78,7 +78,7 @@ final class JwtTokenValidator
throw new InvalidJwtTokenException('Token was not issued for this platform.', 1603123706);
}
if ($token->getClaim('sub') !== JwtTokenGenerator::JWT_SUB) {
if ($token->getClaim('sub') !== TokenGenerator::JWT_SUB) {
throw new InvalidJwtTokenException('Token does not relate to this subject.', 1603123707);
}
......@@ -91,11 +91,11 @@ final class JwtTokenValidator
throw new InvalidJwtTokenException('Token is either expired or not yet ready to be used.', 1603123708);
}
if ($tokenType === 'access_token' && $token->hasClaim('client')) {
if ($tokenType === 'access_token' && $token->hasClaim('user')) {
$accessToken = $this->accessTokenRepository->findById((string)$token->getClaim('jti'));
if ($accessToken === null
|| $accessToken->getExpires() < $timestamp
|| $accessToken->getClient() !== (int)$token->getClaim('client')
|| $accessToken->getUser() !== (int)$token->getClaim('user')
|| $accessToken->getExpires() !== (int)$token->getClaim('exp')
) {
throw new InvalidJwtTokenException('Access token is not valid.', 1603123709);
......@@ -121,7 +121,7 @@ final class JwtTokenValidator
throw new InvalidJwtTokenException('Token misses required claims or is of wrong type. Expected: ' . $tokenType, 1603123713);
}
if ($client !== null && $client->getId() !== $accessToken->getClient()) {
if ($user !== null && $user->getId() !== $accessToken->getUser()) {
throw new InvalidJwtTokenException('Token does not belong to the given user.', 1603123714);
}
......
......@@ -18,19 +18,19 @@ use T3o\TerRest\Authentication\AuthenticationHandlerInterface;
use T3o\TerRest\Authentication\User\ApiUserFactory;
use T3o\TerRest\Authentication\User\ApiUserInterface;
use T3o\TerRest\Exception\InvalidJwtTokenException;
use T3o\TerRest\OAuth2\JwtTokenValidator;
use T3o\TerRest\Authentication\Jwt\TokenValidator;
/**
* Authentication provider for OAuth2 client credentials
* Authentication provider for bearer auth
*/
final class OAuth2ClientCredentials extends AbstractTokenBasedProvider implements AuthenticationProviderInterface
final class BearerAuth extends AbstractTokenBasedProvider implements AuthenticationProviderInterface
{
protected ApiUserFactory $apiUserFactory;
protected JwtTokenValidator $jwtTokenValidator;
protected TokenValidator $jwtTokenValidator;
public function __construct(
ApiUserFactory $apiUserFactory,
JwtTokenValidator $jwtTokenValidator
TokenValidator $jwtTokenValidator
) {
$this->apiUserFactory = $apiUserFactory;
$this->jwtTokenValidator = $jwtTokenValidator;
......@@ -50,7 +50,7 @@ final class OAuth2ClientCredentials extends AbstractTokenBasedProvider implement
try {
$this->jwtTokenValidator->validate($jwtToken, (string)$request->getAttribute('site')->getBase());
return $this->apiUserFactory->createApiUserFromOAuth2($jwtToken);
return $this->apiUserFactory->createApiUserFromToken($jwtToken);
} catch (InvalidJwtTokenException|\OutOfBoundsException|\InvalidArgumentException|\RuntimeException $e) {
// We simply ignore validation errors here as the next authentication handler should hook in
}
......
......@@ -16,7 +16,7 @@ use Lcobucci\JWT\Token;
use T3o\TerRest\Authentication\Scope;
use T3o\TerRest\Exception\InvalidCredentialsException;
use T3o\TerRest\Exception\NoFrontendUserForAccessTokenException;
use T3o\TerRest\OAuth2\Repository\AccessTokenRepository;
use T3o\TerRest\Authentication\Bearer\Repository\AccessTokenRepository;
use T3o\TerRest\Repository\ExtensionRepository;
use T3o\TerRest\Repository\FrontendUserRepository;
......@@ -68,7 +68,7 @@ final class ApiUserFactory
);
}
public function createApiUserFromOAuth2(Token $token): ApiUserInterface
public function createApiUserFromToken(Token $token): ApiUserInterface
{
$accessToken = $this->accessTokenRepository->findById((string)$token->getClaim('jti'));
......@@ -78,7 +78,7 @@ final class ApiUserFactory
throw new InvalidCredentialsException('Authentication not possible.', 1602699229);
}
$frontendUser = $this->frontendUserRepository->findByUserId($accessToken->getClient());
$frontendUser = $this->frontendUserRepository->findByUserId($accessToken->getUser());
if ($frontendUser === []) {
// This factory method should however only be called if the token is already
......
......@@ -152,9 +152,9 @@ abstract class AbstractController implements RequestHandlerInterface
);
}
if (isset($security[RouteConfiguration::OAUTH2_CLIENT_CREDENTIALS])) {
if (isset($security[RouteConfiguration::BEARER_AUTH])) {
$requiredScope = Scope::EXTENSION_NONE;
foreach ($security[RouteConfiguration::OAUTH2_CLIENT_CREDENTIALS] as $scopeName) {
foreach ($security[RouteConfiguration::BEARER_AUTH] as $scopeName) {
if (Scope::hasScope($scopeName)) {
$requiredScope += Scope::getScope($scopeName);
}
......
......@@ -14,13 +14,13 @@ namespace T3o\TerRest\Controller;
use Psr\Http\Message\ResponseInterface;
use T3o\TerRest\Http\ResponseFactory;
use T3o\TerRest\OAuth2\Grant\GrantFactory;
use T3o\TerRest\Authentication\Bearer\Grant\GrantFactory;
use T3o\TerRest\Routing\RouteConfiguration;
/**
* Request handler for `/oauth` endpoint
* Request handler for `/auth` endpoint
*/
final class OauthController extends AbstractController
final class AuthController extends AbstractController
{
protected GrantFactory $grantFactory;
......@@ -33,20 +33,25 @@ final class OauthController extends AbstractController
$this->grantFactory = $grantFactory;
}
public function generateAccessToken(): ResponseInterface
public function issueToken(): ResponseInterface
{
$grantType = $this->getRouteArgumentValue('grant_type');
return $this->processGrant();
}
if (!$this->grantFactory->hasGrantType($grantType)) {
return $this->responseFactory
->createUnauthorizedResponse($this->request, 1603106145, 'unsupported_grant_type', $grantType . ' is not implemented.');
}
public function refreshToken(): ResponseInterface
{
return $this->processGrant();
}
return $this->grantFactory->create($grantType)->handle($this->request);
public function revokeToken(): ResponseInterface
{
return $this->processGrant();
}
public function revokeAccessToken(): ResponseInterface
protected function processGrant(): ResponseInterface
{
return $this->grantFactory->create('revoke_token')->handle($this->request);
return $this->grantFactory
->createFromOperationId($this->getOffset('operationId'))
->handle($this->request);