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