Loading openid_connect.routing.yml +9 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ openid_connect.redirect_controller_redirect: _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::authenticate' _title: 'OpenID Connect redirect page' requirements: _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::access' _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessAuthenticate' options: _maintenance_access: TRUE Loading Loading @@ -91,3 +91,11 @@ openid_connect.login: _user_is_logged_in: 'FALSE' options: _maintenance_access: TRUE openid_connect.redirect_controller_redirect_iss: path: '/openid-connect/{openid_connect_client}/initiate' defaults: _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::initiate' _title: 'OpenID Connect initiate redirect page' requirements: _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessInitiate' src/Controller/OpenIDConnectRedirectController.php +135 −4 Original line number Diff line number Diff line Loading @@ -17,11 +17,14 @@ use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\externalauth\AuthmapInterface; use Drupal\openid_connect\OpenIDConnectClaims; use Drupal\openid_connect\OpenIDConnect; use Drupal\openid_connect\OpenIDConnectClientEntityInterface; use Drupal\openid_connect\OpenIDConnectSessionInterface; use Drupal\openid_connect\OpenIDConnectStateTokenInterface; use Drupal\openid_connect\Plugin\OpenIDConnectClientInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; Loading Loading @@ -108,6 +111,13 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac */ protected $entityTypeManager; /** * The OpenID Connect claims. * * @var \Drupal\openid_connect\OpenIDConnectClaims */ protected $claims; /** * The constructor. * Loading @@ -131,8 +141,10 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac * The language manager service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\openid_connect\OpenIDConnectClaims $claims * The OpenID claims service. */ public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager) { public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, OpenIDConnectClaims $claims) { $this->openIDConnect = $openid_connect; $this->stateToken = $state_token; $this->requestStack = $request_stack; Loading @@ -143,6 +155,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac $this->moduleHandler = $module_handler; $this->languageManager = $language_manager; $this->entityTypeManager = $entity_type_manager; $this->claims = $claims; } /** Loading @@ -159,10 +172,48 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac $container->get('current_user'), $container->get('module_handler'), $container->get('language_manager'), $container->get('entity_type.manager') $container->get('entity_type.manager'), $container->get('openid_connect.claims') ); } /** * Access callback: Initiate page. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client * The client. * * @return \Drupal\Core\Access\AccessResultInterface * Whether the acting user is allowed to initiate the authorization. */ public function accessInitiate(OpenIDConnectClientEntityInterface $openid_connect_client): AccessResultInterface { // Only the anonymous user should be able to access redirects. if ($this->currentUser->isAuthenticated()) { return AccessResult::forbidden(); } if (empty($openid_connect_client)) { return AccessResult::forbidden(); } $query = $this->requestStack->getCurrentRequest()->query; // If the iss query parameter exists, the user is attempting SSO directly // from the IDP. $iss = $query->get('iss'); if (!empty($iss)) { // If the iss domain is not in the allowed list, return forbidden. if (!$this->isValidRedirect($openid_connect_client, $iss)) { return AccessResult::forbidden(); } return AccessResult::allowed(); } // Default to forbidden. return AccessResult::forbidden(); } /** * Access callback: Redirect page. * Loading @@ -170,7 +221,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac * Whether the state token matches the previously created one that is stored * in the session. */ public function access(): AccessResultInterface { public function accessAuthenticate(): AccessResultInterface { // Confirm anti-forgery state token. This round-trip verification helps to // ensure that the user, not a malicious script, is making the request. $request = $this->requestStack->getCurrentRequest(); Loading @@ -178,6 +229,8 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac if ($state_token && $this->stateToken->confirm($state_token)) { return AccessResult::allowed(); } // Default to forbidden. return AccessResult::forbidden(); } Loading Loading @@ -264,7 +317,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac // The destination parameter should be a prepared uri and include any query // parameters or fragments already. // // @see \Drupal\openid_connect\OpenIDConnectSession::saveDestination() // @see \Drupal\openid_connect\OpenIDConnectSessionInterface::saveDestination() $session = $this->session->retrieveDestination(); $destination = $session['destination'] ?: $this->configFactory->get('openid_connect.settings')->get('redirect_login'); $langcode = $session['langcode'] ?: $this->languageManager->getCurrentLanguage()->getId(); Loading @@ -274,6 +327,84 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac return new RedirectResponse($redirect); } /** * Initiate an SSO from the IDP. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client * The client. * * @return \Symfony\Component\HttpFoundation\Response * A redirect response back to the IDP. */ public function initiate(OpenIDConnectClientEntityInterface $openid_connect_client): Response { if (empty($openid_connect_client)) { throw new AccessDeniedHttpException(); } // Handle the ISS query parameter. $query = $this->requestStack->getCurrentRequest()->query; $iss = $query->get('iss'); $additional_parameters = []; // ISS is required. if (empty($iss)) { throw new AccessDeniedHttpException(); } // If the iss domain is not in the allowed list, return access denied. if (!$this->isValidRedirect($openid_connect_client, $iss)) { throw new AccessDeniedHttpException(); } // Handle the login_hint parameter. $login_hint = $query->get('login_hint'); if (!empty($login_hint)) { $additional_parameters['login_hint'] = $login_hint; } // Handle the target_link_uri parameter. if (!empty($query->get('target_link_uri'))) { $this->session->saveTargetLinkUri($query->get('target_link_uri')); } $scopes = $this->claims->getScopes(); return $openid_connect_client->getPlugin()->authorize($scopes, $additional_parameters); } /** * Validate if the domain that initiated the SSO is valid. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $client * The client that was requested. * @param string $iss * The iss url parameter the requested SSO. * * @return bool * True if the domain is in the allowed list. */ private function isValidRedirect(OpenIDConnectClientEntityInterface $client, string $iss): bool { $settings = $client->get('settings'); if (empty($settings['iss_allowed_domains'])) { return FALSE; } // Get the domains that are allowed. $allowed_domains = explode("\r\n", $settings['iss_allowed_domains']); $url = parse_url($iss); if (empty($url['host'])) { return FALSE; } if (in_array($url['host'], $allowed_domains)) { return TRUE; } return FALSE; } /** * Redirect after logout. */ Loading src/OpenIDConnectSession.php +19 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,25 @@ class OpenIDConnectSession implements OpenIDConnectSessionInterface { $this->session->set('openid_connect_langcode', $langcode); } /** * {@inheritDoc} */ public function saveTargetLinkUri(string $target_link_uri): void { try { $uri = Url::fromUserInput($target_link_uri); } catch (\InvalidArgumentException $e) { // Invalid url, return. return; } // Make sure the uri is not external. if (!$uri->isExternal()) { // Save the path if it is safe. $this->session->set('openid_connect_destination', ltrim($target_link_uri, '/')); } } /** * {@inheritdoc} */ Loading src/OpenIDConnectSessionInterface.php +13 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,19 @@ interface OpenIDConnectSessionInterface extends ContainerInjectionInterface { */ public function saveDestination(); /** * Save a target_link_uri as the redirect destination in the session. * * This will convert the user provided string to a \Drupal\Core\Url object * and will ensure it is not an external link before saving the destination * parameter. The string passed _must_ begin with a '/'. * * @param string $target_link_uri * The internal url that the user should be redirected after login. * The string must begin with a '/'. */ public function saveTargetLinkUri(string $target_link_uri): void; /** * Get the operation details from the session. * Loading src/Plugin/OpenIDConnectClient/OpenIDConnectFacebookClient.php +1 −1 Original line number Diff line number Diff line Loading @@ -80,7 +80,7 @@ class OpenIDConnectFacebookClient extends OpenIDConnectClientBase { /** * {@inheritdoc} */ public function authorize(string $scope = 'openid email'): Response { public function authorize(string $scope = 'openid email', array $additional_params = []): Response { // Use Facebook specific authorisations. return parent::authorize('public_profile email'); } Loading Loading
openid_connect.routing.yml +9 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,7 @@ openid_connect.redirect_controller_redirect: _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::authenticate' _title: 'OpenID Connect redirect page' requirements: _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::access' _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessAuthenticate' options: _maintenance_access: TRUE Loading Loading @@ -91,3 +91,11 @@ openid_connect.login: _user_is_logged_in: 'FALSE' options: _maintenance_access: TRUE openid_connect.redirect_controller_redirect_iss: path: '/openid-connect/{openid_connect_client}/initiate' defaults: _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::initiate' _title: 'OpenID Connect initiate redirect page' requirements: _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessInitiate'
src/Controller/OpenIDConnectRedirectController.php +135 −4 Original line number Diff line number Diff line Loading @@ -17,11 +17,14 @@ use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\externalauth\AuthmapInterface; use Drupal\openid_connect\OpenIDConnectClaims; use Drupal\openid_connect\OpenIDConnect; use Drupal\openid_connect\OpenIDConnectClientEntityInterface; use Drupal\openid_connect\OpenIDConnectSessionInterface; use Drupal\openid_connect\OpenIDConnectStateTokenInterface; use Drupal\openid_connect\Plugin\OpenIDConnectClientInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; Loading Loading @@ -108,6 +111,13 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac */ protected $entityTypeManager; /** * The OpenID Connect claims. * * @var \Drupal\openid_connect\OpenIDConnectClaims */ protected $claims; /** * The constructor. * Loading @@ -131,8 +141,10 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac * The language manager service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\openid_connect\OpenIDConnectClaims $claims * The OpenID claims service. */ public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager) { public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, OpenIDConnectClaims $claims) { $this->openIDConnect = $openid_connect; $this->stateToken = $state_token; $this->requestStack = $request_stack; Loading @@ -143,6 +155,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac $this->moduleHandler = $module_handler; $this->languageManager = $language_manager; $this->entityTypeManager = $entity_type_manager; $this->claims = $claims; } /** Loading @@ -159,10 +172,48 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac $container->get('current_user'), $container->get('module_handler'), $container->get('language_manager'), $container->get('entity_type.manager') $container->get('entity_type.manager'), $container->get('openid_connect.claims') ); } /** * Access callback: Initiate page. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client * The client. * * @return \Drupal\Core\Access\AccessResultInterface * Whether the acting user is allowed to initiate the authorization. */ public function accessInitiate(OpenIDConnectClientEntityInterface $openid_connect_client): AccessResultInterface { // Only the anonymous user should be able to access redirects. if ($this->currentUser->isAuthenticated()) { return AccessResult::forbidden(); } if (empty($openid_connect_client)) { return AccessResult::forbidden(); } $query = $this->requestStack->getCurrentRequest()->query; // If the iss query parameter exists, the user is attempting SSO directly // from the IDP. $iss = $query->get('iss'); if (!empty($iss)) { // If the iss domain is not in the allowed list, return forbidden. if (!$this->isValidRedirect($openid_connect_client, $iss)) { return AccessResult::forbidden(); } return AccessResult::allowed(); } // Default to forbidden. return AccessResult::forbidden(); } /** * Access callback: Redirect page. * Loading @@ -170,7 +221,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac * Whether the state token matches the previously created one that is stored * in the session. */ public function access(): AccessResultInterface { public function accessAuthenticate(): AccessResultInterface { // Confirm anti-forgery state token. This round-trip verification helps to // ensure that the user, not a malicious script, is making the request. $request = $this->requestStack->getCurrentRequest(); Loading @@ -178,6 +229,8 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac if ($state_token && $this->stateToken->confirm($state_token)) { return AccessResult::allowed(); } // Default to forbidden. return AccessResult::forbidden(); } Loading Loading @@ -264,7 +317,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac // The destination parameter should be a prepared uri and include any query // parameters or fragments already. // // @see \Drupal\openid_connect\OpenIDConnectSession::saveDestination() // @see \Drupal\openid_connect\OpenIDConnectSessionInterface::saveDestination() $session = $this->session->retrieveDestination(); $destination = $session['destination'] ?: $this->configFactory->get('openid_connect.settings')->get('redirect_login'); $langcode = $session['langcode'] ?: $this->languageManager->getCurrentLanguage()->getId(); Loading @@ -274,6 +327,84 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac return new RedirectResponse($redirect); } /** * Initiate an SSO from the IDP. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client * The client. * * @return \Symfony\Component\HttpFoundation\Response * A redirect response back to the IDP. */ public function initiate(OpenIDConnectClientEntityInterface $openid_connect_client): Response { if (empty($openid_connect_client)) { throw new AccessDeniedHttpException(); } // Handle the ISS query parameter. $query = $this->requestStack->getCurrentRequest()->query; $iss = $query->get('iss'); $additional_parameters = []; // ISS is required. if (empty($iss)) { throw new AccessDeniedHttpException(); } // If the iss domain is not in the allowed list, return access denied. if (!$this->isValidRedirect($openid_connect_client, $iss)) { throw new AccessDeniedHttpException(); } // Handle the login_hint parameter. $login_hint = $query->get('login_hint'); if (!empty($login_hint)) { $additional_parameters['login_hint'] = $login_hint; } // Handle the target_link_uri parameter. if (!empty($query->get('target_link_uri'))) { $this->session->saveTargetLinkUri($query->get('target_link_uri')); } $scopes = $this->claims->getScopes(); return $openid_connect_client->getPlugin()->authorize($scopes, $additional_parameters); } /** * Validate if the domain that initiated the SSO is valid. * * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $client * The client that was requested. * @param string $iss * The iss url parameter the requested SSO. * * @return bool * True if the domain is in the allowed list. */ private function isValidRedirect(OpenIDConnectClientEntityInterface $client, string $iss): bool { $settings = $client->get('settings'); if (empty($settings['iss_allowed_domains'])) { return FALSE; } // Get the domains that are allowed. $allowed_domains = explode("\r\n", $settings['iss_allowed_domains']); $url = parse_url($iss); if (empty($url['host'])) { return FALSE; } if (in_array($url['host'], $allowed_domains)) { return TRUE; } return FALSE; } /** * Redirect after logout. */ Loading
src/OpenIDConnectSession.php +19 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,25 @@ class OpenIDConnectSession implements OpenIDConnectSessionInterface { $this->session->set('openid_connect_langcode', $langcode); } /** * {@inheritDoc} */ public function saveTargetLinkUri(string $target_link_uri): void { try { $uri = Url::fromUserInput($target_link_uri); } catch (\InvalidArgumentException $e) { // Invalid url, return. return; } // Make sure the uri is not external. if (!$uri->isExternal()) { // Save the path if it is safe. $this->session->set('openid_connect_destination', ltrim($target_link_uri, '/')); } } /** * {@inheritdoc} */ Loading
src/OpenIDConnectSessionInterface.php +13 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,19 @@ interface OpenIDConnectSessionInterface extends ContainerInjectionInterface { */ public function saveDestination(); /** * Save a target_link_uri as the redirect destination in the session. * * This will convert the user provided string to a \Drupal\Core\Url object * and will ensure it is not an external link before saving the destination * parameter. The string passed _must_ begin with a '/'. * * @param string $target_link_uri * The internal url that the user should be redirected after login. * The string must begin with a '/'. */ public function saveTargetLinkUri(string $target_link_uri): void; /** * Get the operation details from the session. * Loading
src/Plugin/OpenIDConnectClient/OpenIDConnectFacebookClient.php +1 −1 Original line number Diff line number Diff line Loading @@ -80,7 +80,7 @@ class OpenIDConnectFacebookClient extends OpenIDConnectClientBase { /** * {@inheritdoc} */ public function authorize(string $scope = 'openid email'): Response { public function authorize(string $scope = 'openid email', array $additional_params = []): Response { // Use Facebook specific authorisations. return parent::authorize('public_profile email'); } Loading