Skip to content

Instantly share code, notes, and snippets.

@bwaidelich
Last active May 13, 2020 11:54
Show Gist options
  • Save bwaidelich/0932b015cfffd20ef40c919a78c439a8 to your computer and use it in GitHub Desktop.
Save bwaidelich/0932b015cfffd20ef40c919a78c439a8 to your computer and use it in GitHub Desktop.
External authentication with Neos Flow and local JWT (http://jwt.io/) as cache
<?php
declare(strict_types=1);
namespace Your\Package\Security\Authentication;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Security\Authentication\Token\AbstractToken;
use Neos\Flow\Security\Authentication\Token\SessionlessTokenInterface;
/**
* An authentication token used to fetch JWT credentials from a cookie
*/
class Jwt extends AbstractToken implements SessionlessTokenInterface
{
/**
* The jwt credentials
*
* @var array
* @Flow\Transient
*/
protected $credentials = ['jwt' => ''];
/**
* @param ActionRequest $actionRequest The current action request
* @return void
*/
public function updateCredentials(ActionRequest $actionRequest)
{
$jwtCookie = $actionRequest->getHttpRequest()->getCookie('jwt');
if ($jwtCookie === null) {
return;
}
$this->credentials['jwt'] = $jwtCookie->getValue();
$this->setAuthenticationStatus(self::AUTHENTICATION_NEEDED);
}
/**
* Returns a string representation of the token for logging purposes.
*
* @return string The username credential
*/
public function __toString()
{
return 'JWT: "' . substr($this->credentials['jwt'], 0, 10) . '..."';
}
}
<?php
declare(strict_types=1);
namespace Your\Package;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Http\Cookie;
use Neos\Flow\Http\HttpRequestHandlerInterface;
use Neos\Flow\Package\Package as BasePackage;
use Neos\Flow\Security\Authentication\AuthenticationProviderManager;
class Package extends BasePackage
{
/**
* @param Bootstrap $bootstrap The current bootstrap
* @return void
*/
public function boot(Bootstrap $bootstrap)
{
$dispatcher = $bootstrap->getSignalSlotDispatcher();
// expire JWT cookie when the user logs out
$dispatcher->connect(
AuthenticationProviderManager::class, 'loggedOut',
function() use ($bootstrap) {
$requestHandler = $bootstrap->getActiveRequestHandler();
// not a HTTP request handler? => none of our business
if (!$requestHandler instanceof HttpRequestHandlerInterface) {
return;
}
$jwtCookie = new Cookie('jwt');
$jwtCookie->expire();
$requestHandler->getHttpResponse()->setCookie($jwtCookie);
}
);
}
}
<?php
declare(strict_types=1);
namespace Your\Package\Security\Authentication;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Security\Authentication\Token\SessionlessTokenInterface;
use Neos\Flow\Security\Authentication\Token\UsernamePassword;
/**
* An authentication token used for simple username and password authentication (sessionless)
*/
class SessionlessUsernamePassword extends UsernamePassword implements SessionlessTokenInterface
{
}
Neos:
Flow:
security:
authentication:
providers:
'mySsoProvider':
provider: 'Your\Package\Security\Authentication\SsoJwtProvider'
providerOptions:
# optional lifetime for JWT cookies (if omitted the JWT stays active until browser session ends, or user explicitly logs out)
tokenLifetime: 3600
token: 'Your\Package\Security\Authentication\SessionlessUsernamePassword'
'jwtProvider':
provider: 'Your\Package\Security\Authentication\SsoJwtProvider'
token: 'Your\Package\Security\Authentication\Jwt'
<?php
declare(strict_types=1);
namespace Your\Package\Security\Authentication;
use Firebase\JWT\JWT as JwtService;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Http\Cookie;
use Neos\Flow\Http\HttpRequestHandlerInterface;
use Neos\Flow\Log\SystemLoggerInterface;
use Neos\Flow\Security\Account;
use Neos\Flow\Security\Authentication\Provider\AbstractProvider;
use Neos\Flow\Security\Authentication\Token\UsernamePassword;
use Neos\Flow\Security\Authentication\Token\UsernamePasswordHttpBasic;
use Neos\Flow\Security\Authentication\TokenInterface;
use Neos\Flow\Security\Cryptography\HashService;
use Neos\Flow\Security\Exception\UnsupportedAuthenticationTokenException;
use Neos\Flow\Security\Policy\PolicyService;
/**
* An authentication provider that authenticates Jwt and UsernamePassword tokens.
*/
class SsoJwtProvider extends AbstractProvider
{
/**
* @Flow\Inject
* @var PolicyService
*/
protected $policyService;
/**
* @Flow\Inject
* @var HashService
*/
protected $hashService;
/**
* @Flow\Inject
* @var SystemLoggerInterface
*/
protected $systemLogger;
/**
* Returns the class names of the tokens this provider can authenticate.
*
* @return array
*/
public function getTokenClassNames()
{
return [Jwt::class, SessionlessUsernamePassword::class, UsernamePasswordHttpBasic::class];
}
/**
* Checks the given token for validity and sets the token authentication status
* accordingly (success, wrong credentials or no credentials given).
*
* @param TokenInterface $authenticationToken The token to be authenticated
* @return void
* @throws UnsupportedAuthenticationTokenException
*/
public function authenticate(TokenInterface $authenticationToken)
{
if ($authenticationToken instanceof UsernamePassword) {
$this->authenticateUsernamePassword($authenticationToken);
} elseif ($authenticationToken instanceof Jwt) {
$this->authenticateJwt($authenticationToken);
} else {
throw new UnsupportedAuthenticationTokenException('This provider cannot authenticate the given token.', 1461748226);
}
}
protected function authenticateUsernamePassword(UsernamePassword $token)
{
$credentials = $token->getCredentials();
// TODO: verify credentials, obtain roles
$accountIdentifier = $credentials['username'];
$roleIdentifiers = [];//'Wwwision.Test:SomeRole'];
$account = $this->createTransientAccount($accountIdentifier, $roleIdentifiers);
$token->setAccount($account);
$token->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL);
$this->createJwtCookie($account);
}
protected function authenticateJwt(Jwt $token)
{
$credentials = $token->getCredentials();
if (!is_array($credentials) || !isset($credentials['jwt'])) {
$token->setAuthenticationStatus(TokenInterface::NO_CREDENTIALS_GIVEN);
return;
}
// Don't be surprised by the hard-coded "jwt". That is *not* the secret key of the JWT. HashService::generateHmac() uses the encryption key of this installation
$jwtKey = $this->hashService->generateHmac('jwt');
$jwtPayload = null;
try {
$jwtPayload = (array)JwtService::decode($credentials['jwt'], $jwtKey, ['HS256']);
} catch (\Exception $exception) {
$this->systemLogger->logException($exception);
$token->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS);
return;
}
if ($jwtPayload === null || !isset($jwtPayload['accountIdentifier']) || !isset($jwtPayload['accountRoleIdentifiers'])) {
$token->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS);
return;
}
$account = $this->createTransientAccount($jwtPayload['accountIdentifier'], $jwtPayload['accountRoleIdentifiers']);
$token->setAccount($account);
$token->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL);
}
/**
* @param Account $account
* @return void
*/
protected function createJwtCookie(Account $account)
{
// Note: Retrieving the Response via Bootstrap is a little hacky, but in this case I'd consider it worth it
/** @var Bootstrap $bootstrap */
$bootstrap = Bootstrap::$staticObjectManager->get(Bootstrap::class);
$requestHandler = $bootstrap->getActiveRequestHandler();
// not a HTTP request (e.g. in CLI context)? => No way to set a cookie
if (!$requestHandler instanceof HttpRequestHandlerInterface) {
return;
}
$jwtPayload = [
'accountIdentifier' => $account->getAccountIdentifier(),
'accountRoleIdentifiers' => array_keys($account->getRoles()),
];
$jwtExpiration = isset($this->options['tokenLifetime']) ? time() + (integer)$this->options['tokenLifetime'] : 0;
if ($jwtExpiration > 0) {
$jwtPayload['exp'] = $jwtExpiration;
}
// Don't be surprised by the hard-coded "jwt". That is *not* the secret key of the JWT. HashService::generateHmac() uses the encryption key of this installation
$jwtKey = $this->hashService->generateHmac('jwt');
$jwt = JwtService::encode($jwtPayload, $jwtKey, 'HS256');
$jwtCookie = new Cookie('jwt', $jwt, $jwtExpiration);
$requestHandler->getHttpResponse()->setCookie($jwtCookie);
}
/**
* @param $accountIdentifier
* @param array $roleIdentifiers
* @return Account
*/
protected function createTransientAccount($accountIdentifier, array $roleIdentifiers)
{
$account = new Account();
$account->setAccountIdentifier($accountIdentifier);
foreach ($roleIdentifiers as $roleIdentifier) {
$account->addRole($this->policyService->getRole($roleIdentifier));
}
$account->setAuthenticationProviderName($this->name);
return $account;
}
}
@davidspiola
Copy link

Hi Bastian, you mentioned in the last Neos CMS Online Meetup that it might be much more comfortable with the Neos 5.2 to use JWT with Neos CMS. Do you still recommend using this code?

@bwaidelich
Copy link
Author

@davidspiola With Neos 5.2 you can now pass options to the Token. For example to make the Cookie name configurable or the way the token is extracted from the request in general.
With neos/flow-development-collection#1993 you'll be able to reuse the auth provider (reviews welcome *g).

Apart from that I never recommended to use this code, but if it works for you that's great :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment