Last active
May 13, 2020 11:54
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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) . '..."'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 | |
{ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 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
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?