From 35d1eebedbba9141799804817e1c92043ad6ac5c Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Tue, 30 Jul 2019 13:07:36 +0200 Subject: [PATCH] Logout via Query Params * Remove "cookieName" configuration and assume "-jwt" as cookie name * Adjust `SetJwtCookieComponent` to handle logout (remove JWT cookie and trigger redirect) if `?logout=` query parameter is present Closes: #2 --- .../Authentication/OpenIdConnectProvider.php | 9 +- Classes/Http/SetJwtCookieComponent.php | 51 ++++++- Configuration/Settings.Http.yaml | 2 +- Configuration/Settings.yaml | 4 +- README.md | 135 +++++++++--------- 5 files changed, 119 insertions(+), 82 deletions(-) diff --git a/Classes/Authentication/OpenIdConnectProvider.php b/Classes/Authentication/OpenIdConnectProvider.php index e2acdd8..13f70eb 100644 --- a/Classes/Authentication/OpenIdConnectProvider.php +++ b/Classes/Authentication/OpenIdConnectProvider.php @@ -68,12 +68,11 @@ public function authenticate(TokenInterface $authenticationToken): void if (!isset($this->options['accountIdentifierTokenValueName'])) { $this->options['accountIdentifierTokenValueName'] = 'sub'; } - if (!isset($this->options['jwtCookieName'])) { - $this->options['jwtCookieName'] = 'flownative_oidc_jwt'; - } + $serviceName = $this->options['serviceName']; + $jwtCookieName = $serviceName . '-jwt'; try { - $jwks = (new OpenIdConnectClient($this->options['serviceName']))->getJwks(); - $identityToken = $authenticationToken->extractIdentityTokenFromRequest($this->options['jwtCookieName']); + $jwks = (new OpenIdConnectClient($serviceName))->getJwks(); + $identityToken = $authenticationToken->extractIdentityTokenFromRequest($jwtCookieName); if (!$identityToken->hasValidSignature($jwks)) { throw new SecurityException(sprintf('Open ID Connect: The identity token provided by the OIDC provider had an invalid signature'), 1561479176); } diff --git a/Classes/Http/SetJwtCookieComponent.php b/Classes/Http/SetJwtCookieComponent.php index 89d8254..753032e 100644 --- a/Classes/Http/SetJwtCookieComponent.php +++ b/Classes/Http/SetJwtCookieComponent.php @@ -3,13 +3,13 @@ namespace Flownative\OpenIdConnect\Client\Http; use Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken; -use Flownative\OpenIdConnect\Client\AuthenticationException; use Flownative\OpenIdConnect\Client\IdentityToken; use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\Component\ComponentContext; use Neos\Flow\Http\Component\ComponentInterface; use Neos\Flow\Http\Cookie; use Neos\Flow\Security\Context as SecurityContext; +use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; final class SetJwtCookieComponent implements ComponentInterface @@ -41,8 +41,12 @@ public function __construct(array $options = null) */ public function handle(ComponentContext $componentContext): void { + if ($this->isLogoutRequest($componentContext)) { + $this->handleLogout($componentContext); + return; + } if (!$this->securityContext->isInitialized() && !$this->securityContext->canBeInitialized()) { - $this->logger->debug(sprintf('OpenID Connect Client: (%s) Cannot send JWT cookie because the security context could not be initialized.', get_class($this))); + $this->logger->debug(sprintf('OpenID Connect Client: (%s) Cannot send JWT cookie because the security context could not be initialized for service "%s".', get_class($this), $this->options['serviceName'])); return; } if (!$this->isOpenIdConnectAuthentication()) { @@ -51,14 +55,19 @@ public function handle(ComponentContext $componentContext): void $account = $this->securityContext->getAccountByAuthenticationProviderName($this->options['authenticationProviderName']); if ($account === null) { - $this->logger->info(sprintf('OpenID Connect Client: (%s) No Flow account found for %s, removing JWT cookie.', get_class($this), $this->options['authenticationProviderName'])); + $this->logger->info(sprintf('OpenID Connect Client: (%s) No Flow account found for %s, removing JWT cookie for service "%s".', get_class($this), $this->options['authenticationProviderName'], $this->options['serviceName'])); + $this->removeJwtCookie($componentContext); + return; + } + if ($this->isLogoutRequest($componentContext)) { $this->removeJwtCookie($componentContext); + $this->logger->info(sprintf('OpenID Connect Client: (%s) Logout requested (via query parameter) removing JWT cookie for service "%s".', get_class($this), $this->options['serviceName'])); return; } $identityToken = $account->getCredentialsSource(); if (!$identityToken instanceof IdentityToken) { - $this->logger->error(sprintf('OpenID Connect Client: (%s) No identity token found in credentials source of account %s - could not set JWT cookie.', get_class($this), $account->getAccountIdentifier())); + $this->logger->error(sprintf('OpenID Connect Client: (%s) No identity token found in credentials source of account %s - could not set JWT cookie for service "%s".', get_class($this), $account->getAccountIdentifier(), $this->options['serviceName'])); return; } @@ -78,13 +87,38 @@ private function isOpenIdConnectAuthentication(): bool return false; } + /** + * @param ComponentContext $componentContext + * @return bool + */ + private function isLogoutRequest(ComponentContext $componentContext): bool + { + $httpRequest = $componentContext->getHttpRequest(); + if (!$httpRequest instanceof ServerRequestInterface) { + return false; + } + $queryParams = $httpRequest->getQueryParams(); + return isset($queryParams['logout']) && $queryParams['logout'] === $this->options['serviceName']; + } + + /** + * @param ComponentContext $componentContext + * @return void + */ + private function handleLogout(ComponentContext $componentContext): void + { + $this->removeJwtCookie($componentContext); + $this->logger->info(sprintf('OpenID Connect Client: (%s) Logout requested (via query parameter) removing JWT cookie for service "%s".', get_class($this), $this->options['serviceName'])); + $componentContext->replaceHttpResponse($componentContext->getHttpResponse()->withHeader('Location', (string)$componentContext->getHttpRequest()->getUri()->withQuery(''))); + } + /** * @param ComponentContext $componentContext * @param string $jwt */ private function setJwtCookie(ComponentContext $componentContext, string $jwt): void { - $jwtCookie = new Cookie($this->options['cookieName'], $jwt, 0, null, null, '/', $this->options['secureCookie'], false); + $jwtCookie = new Cookie($this->jwtCookieName(), $jwt, 0, null, null, '/', $this->options['secureCookie'], false); $componentContext->replaceHttpResponse($componentContext->getHttpResponse()->withAddedHeader('Set-Cookie', (string)$jwtCookie)); } @@ -93,7 +127,12 @@ private function setJwtCookie(ComponentContext $componentContext, string $jwt): */ private function removeJwtCookie(ComponentContext $componentContext): void { - $emptyJwtCookie = new Cookie($this->options['cookieName'], '', 1, null, null, '/', $this->options['secureCookie'], false); + $emptyJwtCookie = new Cookie($this->jwtCookieName(), '', 1, null, null, '/', $this->options['secureCookie'], false); $componentContext->replaceHttpResponse($componentContext->getHttpResponse()->withAddedHeader('Set-Cookie', (string)$emptyJwtCookie)); } + + private function jwtCookieName(): string + { + return $this->options['serviceName'] . '-jwt'; + } } diff --git a/Configuration/Settings.Http.yaml b/Configuration/Settings.Http.yaml index 17b4adc..9dce30f 100644 --- a/Configuration/Settings.Http.yaml +++ b/Configuration/Settings.Http.yaml @@ -8,6 +8,6 @@ Neos: 'position': 'after setSessionCookie' component: 'Flownative\OpenIdConnect\Client\Http\SetJwtCookieComponent' componentOptions: - cookieName: 'flownative_oidc_jwt' + serviceName: 'flownative_oidc' secureCookie: true authenticationProviderName: 'Flownative.OpenIdConnect.Client:OidcProvider' diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 05071d3..fa6f1ae 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -16,7 +16,8 @@ Flownative: # chain: # 'Flownative.OpenIdConnect.Client:setJwtCookie': # componentOptions: -# cookieName: 'your-own-cookie-name-jwt' +# # Use the same service name as below - the corresponding JWT will be stored in a cookie named "-jwt" +# serviceName: 'exampleService' # security: # authentication: @@ -27,7 +28,6 @@ Flownative: # providerOptions: # roles: ['Acme.MyPackage:User'] # accountIdentifierTokenValueName: 'inum' -# jwtCookieName: 'your-own-cookie-name-jwt' # serviceName: 'exampleService' # token: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken' # requestPatterns: diff --git a/README.md b/README.md index ff706c1..bcb2fa9 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,67 @@ -[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) -[![Packagist](https://img.shields.io/packagist/v/flownative/openidconnect-client.svg)](https://packagist.org/packages/flownative/openidconnect-client) -[![Maintenance level: Love](https://img.shields.io/badge/maintenance-%E2%99%A1%E2%99%A1%E2%99%A1-ff69b4.svg)](https://www.flownative.com/en/products/open-source.html) - -# OpenID Connect Client for Flow Framework - -This [Flow](https://flow.neos.io) package provides an [OpenID Connect](https://openid.net/connect/) client SDK. - -Note: this package is at an early stage, breaking changes may be released without further warning ... - -## Configuration - - Flownative: - OpenIdConnect: - Client: - services: [] - exampleService: - options: - discoveryUri: 'https://example.com/.well-known/openid-configuration' - clientId: '@!EDD5.370D.8247.FED9!0001!B1C9.92C1!1008!13DB.54D8.65DE.2761' - clientSecret: 'very-secret' - - Neos: - Flow: - http: - chain: - 'postprocess': - chain: - 'Flownative.OpenIdConnect.Client:setJwtCookie': - componentOptions: - cookieName: 'your-own-cookie-name-jwt' - secureCookie: false - - security: - authentication: - providers: - 'Flownative.OpenIdConnect.Client:OidcProvider': - label: 'OpenID Connect' - provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider' - providerOptions: - roles: ['Acme.MyPackage:User'] - accountIdentifierTokenValueName: 'inum' - jwtCookieName: 'your-own-cookie-name-jwt' - serviceName: 'exampleService' - token: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken' - requestPatterns: - 'Acme.MyPackage:Frontend': - pattern: 'Acme\MyPackage\Security\SiteRequestPattern' - patternOptions: - 'siteNodeName': 'mysite' - 'matchFrontend': true - entryPoint: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectEntryPoint' - entryPointOptions: - serviceName: 'acmeservice' - scopes: ['inum', 'user_name', 'sap_custno'] - -## Authentication Flow - -Authentication works as follows: - -... - -## About OpenID Connect - -See also: - -https://openid.net/specs/openid-connect-basic-1_0.html -https://connect2id.com/learn/openid-connect +[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) +[![Packagist](https://img.shields.io/packagist/v/flownative/openidconnect-client.svg)](https://packagist.org/packages/flownative/openidconnect-client) +[![Maintenance level: Love](https://img.shields.io/badge/maintenance-%E2%99%A1%E2%99%A1%E2%99%A1-ff69b4.svg)](https://www.flownative.com/en/products/open-source.html) + +# OpenID Connect Client for Flow Framework + +This [Flow](https://flow.neos.io) package provides an [OpenID Connect](https://openid.net/connect/) client SDK. + +Note: this package is at an early stage, breaking changes may be released without further warning ... + +## Configuration + + Flownative: + OpenIdConnect: + Client: + services: [] + exampleService: + options: + discoveryUri: 'https://example.com/.well-known/openid-configuration' + clientId: '@!EDD5.370D.8247.FED9!0001!B1C9.92C1!1008!13DB.54D8.65DE.2761' + clientSecret: 'very-secret' + + Neos: + Flow: + http: + chain: + 'postprocess': + chain: + 'Flownative.OpenIdConnect.Client:setJwtCookie': + componentOptions: + serviceName: 'acmeservice' + secureCookie: false + + security: + authentication: + providers: + 'Flownative.OpenIdConnect.Client:OidcProvider': + label: 'OpenID Connect' + provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider' + providerOptions: + roles: ['Acme.MyPackage:User'] + accountIdentifierTokenValueName: 'inum' + serviceName: 'acmeservice' + token: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken' + requestPatterns: + 'Acme.MyPackage:Frontend': + pattern: 'Acme\MyPackage\Security\SiteRequestPattern' + patternOptions: + 'siteNodeName': 'mysite' + 'matchFrontend': true + entryPoint: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectEntryPoint' + entryPointOptions: + serviceName: 'acmeservice' + scopes: ['inum', 'user_name', 'sap_custno'] + +## Authentication Flow + +Authentication works as follows: + +... + +## About OpenID Connect + +See also: + +https://openid.net/specs/openid-connect-basic-1_0.html +https://connect2id.com/learn/openid-connect