From 8aaffe8ff5ec7381de1bfd1bc2b3407ba61cec59 Mon Sep 17 00:00:00 2001 From: John Franklin <john.franklin@bixal.com> Date: Tue, 13 Sep 2022 20:50:26 -0400 Subject: [PATCH] Issue #3308944 by John Franklin: verify the nonce returned by login.gov. --- .../OpenIDConnectLoginGovClient.php | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Plugin/OpenIDConnectClient/OpenIDConnectLoginGovClient.php b/src/Plugin/OpenIDConnectClient/OpenIDConnectLoginGovClient.php index b2a1f41..75f6436 100644 --- a/src/Plugin/OpenIDConnectClient/OpenIDConnectLoginGovClient.php +++ b/src/Plugin/OpenIDConnectClient/OpenIDConnectLoginGovClient.php @@ -2,12 +2,14 @@ namespace Drupal\login_gov\Plugin\OpenIDConnectClient; +use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Crypt; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\GeneratedUrl; use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\openid_connect\Plugin\OpenIDConnectClientBase; +use Firebase\JWT\JWK; use Firebase\JWT\JWT; /** @@ -198,12 +200,14 @@ class OpenIDConnectLoginGovClient extends OpenIDConnectClientBase { 'token' => 'https://idp.int.identitysandbox.gov/api/openid_connect/token', 'userinfo' => 'https://idp.int.identitysandbox.gov/api/openid_connect/userinfo', 'end_session' => 'https://idp.int.identitysandbox.gov/openid_connect/logout', + 'certs' => 'https://idp.int.identitysandbox.gov/api/openid_connect/certs', ] : [ 'authorization' => 'https://secure.login.gov/openid_connect/authorize', 'token' => 'https://secure.login.gov/api/openid_connect/token', 'userinfo' => 'https://secure.login.gov/api/openid_connect/userinfo', 'end_session' => 'https://secure.login.gov/openid_connect/logout', + 'certs' => 'https://secure.login.gov/api/openid_connect/certs', ]; } @@ -260,6 +264,19 @@ class OpenIDConnectLoginGovClient extends OpenIDConnectClientBase { return $this->configuration['private_key']; } + /** + * Get login.gov's public signing key. + * + * @return array|null + * A list of public keys. + */ + protected function getPeerPublicKeys(): ?array { + $endpoints = $this->getEndpoints(); + $keys_json = $this->httpClient->get($endpoints['certs'])->getBody()->getContents(); + $keys = Json::decode($keys_json); + return JWK::parseKeySet($keys); + } + /** * Generate a one-time use code word, a nonce. * @@ -268,8 +285,6 @@ class OpenIDConnectLoginGovClient extends OpenIDConnectClientBase { * * @return string * The nonce. - * - * @todo Save the nonce to verify later. */ protected function generateNonce(int $length = 26): string { return substr(Crypt::randomBytesBase64($length), 0, $length); @@ -297,10 +312,13 @@ class OpenIDConnectLoginGovClient extends OpenIDConnectClientBase { */ protected function getUrlOptions(string $scope, GeneratedUrl $redirect_uri): array { $options = parent::getUrlOptions($scope, $redirect_uri); + + $nonce = $this->generateNonce(); $options['query'] += [ 'acr_values' => $this->generateAcrValue(), - 'nonce' => $this->generateNonce(), + 'nonce' => $nonce, ]; + $this->requestStack->getCurrentRequest()->getSession()->set('login_gov.nonce', $nonce); if ($this->configuration['acr_level'] == '2' && $this->configuration['verified_within']['count']) { $options['query']['verified_within'] = $this->configuration['verified_within']['count'] . $this->configuration['verified_within']['units']; @@ -310,6 +328,25 @@ class OpenIDConnectLoginGovClient extends OpenIDConnectClientBase { return $options; } + /** + * {@inheritdoc} + */ + public function retrieveTokens(string $authorization_code): ?array { + $tokens = parent::retrieveTokens($authorization_code); + + // Verify the nonce is the one we sent earlier. + if (!empty($tokens['id_token'])) { + $keys = $this->getPeerPublicKeys(); + $decoded_tokens = JWT::decode($tokens['id_token'], $keys); + $session_nonce = $this->requestStack->getCurrentRequest()->getSession()->get('login_gov.nonce'); + if (!empty($session_nonce) && ($decoded_tokens->nonce !== $session_nonce)) { + return NULL; + } + } + + return $tokens; + } + /** * {@inheritdoc} */ -- GitLab