Repository: getmoto/moto (python)

Issue:
Cognito-IDP missing default claims in access token
[Default claims](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html) are missing in the `create_jwt` method:

https://github.com/getmoto/moto/blob/7536c084790fd8db44ceaa34bb1a3ce757509782/moto/cognitoidp/models.py#L521-L537

When comparing to the AWS docs (see link above), these fields are missing:
- `device_key`
  - not documented on the page I linked to
- `version`
  - not documented on the page I linked to
- `origin_jti`
- `event_id`
- `scope`
- `iat`
- `jti`

Example of the payload extracted from an access token generated by moto:
```json
{
  "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_f3c2ac4ccfdc4ac1a37a22ddc517a47d",
  "sub": "7e4a989e-a8f1-452d-8fad-591e48147f98",
  "client_id": "tiv5vb8a6kjmwk0q8w9h5fu4x5",
  "token_use": "access",
  "auth_time": 1736349176,
  "exp": 1736352776,
  "username": "developer@omitted.com"
}
```

Now my current problem is not having `jti`, because the way things are setup currently requires an access token that has `jti` for checking if the token is revoked.


This is how I generate the token, maybe I'm missing something here or have wrong assumptions about that this should do:

```python
    c_idp.admin_create_user(
        UserPoolId=user_pool_id,
        Username=username,
        UserAttributes=[
            {"Name": "email", "Value": username},
        ],
        TemporaryPassword="Password123",
    )
    res1 = c_idp.admin_initiate_auth(
        UserPoolId=user_pool_id,
        ClientId=pool_client_id,
        AuthFlow="ADMIN_NO_SRP_AUTH",
        AuthParameters={
            "USERNAME": username,
            "PASSWORD": "Password123",
        },
    )

    res2 = c_idp.admin_respond_to_auth_challenge(
        UserPoolId=user_pool_id,
        ClientId=pool_client_id,
        ChallengeName="NEW_PASSWORD_REQUIRED",
        ChallengeResponses={
            "USERNAME": username,
            "NEW_PASSWORD": "Password1234",
        },
        Session=res1["Session"],
    )
    access_token = res2["AuthenticationResult"]["AccessToken"]

```

(`c_idp` is a boto3 client instantiated in the scope of a function decorated with `@mock_aws`)

## Question
Is this left out intentionally or is this just something that was never implemented yet? Am I using the correct way to get an access token, or should I be using other apis?

Please advice.

Pull request:
CognitoIDP: AccessTokens and IDTokens now contain the jti and origin_jti values
Fixes #8477 

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index be482c7dc..68d5289fc 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -8232,6 +8232,91 @@
 - [ ] validate_resource_policy
 </details>
 
+## securityhub
+<details>
+<summary>2% implemented</summary>
+
+- [ ] accept_administrator_invitation
+- [ ] accept_invitation
+- [ ] batch_delete_automation_rules
+- [ ] batch_disable_standards
+- [ ] batch_enable_standards
+- [ ] batch_get_automation_rules
+- [ ] batch_get_configuration_policy_associations
+- [ ] batch_get_security_controls
+- [ ] batch_get_standards_control_associations
+- [X] batch_import_findings
+- [ ] batch_update_automation_rules
+- [ ] batch_update_findings
+- [ ] batch_update_standards_control_associations
+- [ ] create_action_target
+- [ ] create_automation_rule
+- [ ] create_configuration_policy
+- [ ] create_finding_aggregator
+- [ ] create_insight
+- [ ] create_members
+- [ ] decline_invitations
+- [ ] delete_action_target
+- [ ] delete_configuration_policy
+- [ ] delete_finding_aggregator
+- [ ] delete_insight
+- [ ] delete_invitations
+- [ ] delete_members
+- [ ] describe_action_targets
+- [ ] describe_hub
+- [ ] describe_organization_configuration
+- [ ] describe_products
+- [ ] describe_standards
+- [ ] describe_standards_controls
+- [ ] disable_import_findings_for_product
+- [ ] disable_organization_admin_account
+- [ ] disable_security_hub
+- [ ] disassociate_from_administrator_account
+- [ ] disassociate_from_master_account
+- [ ] disassociate_members
+- [ ] enable_import_findings_for_product
+- [ ] enable_organization_admin_account
+- [ ] enable_security_hub
+- [ ] get_administrator_account
+- [ ] get_configuration_policy
+- [ ] get_configuration_policy_association
+- [ ] get_enabled_standards
+- [ ] get_finding_aggregator
+- [ ] get_finding_history
+- [X] get_findings
+- [ ] get_insight_results
+- [ ] get_insights
+- [ ] get_invitations_count
+- [ ] get_master_account
+- [ ] get_members
+- [ ] get_security_control_definition
+- [ ] invite_members
+- [ ] list_automation_rules
+- [ ] list_configuration_policies
+- [ ] list_configuration_policy_associations
+- [ ] list_enabled_products_for_import
+- [ ] list_finding_aggregators
+- [ ] list_invitations
+- [ ] list_members
+- [ ] list_organization_admin_accounts
+- [ ] list_security_control_definitions
+- [ ] list_standards_control_associations
+- [ ] list_tags_for_resource
+- [ ] start_configuration_policy_association
+- [ ] start_configuration_policy_disassociation
+- [ ] tag_resource
+- [ ] untag_resource
+- [ ] update_action_target
+- [ ] update_configuration_policy
+- [ ] update_finding_aggregator
+- [ ] update_findings
+- [ ] update_insight
+- [ ] update_organization_configuration
+- [ ] update_security_control
+- [ ] update_security_hub_configuration
+- [ ] update_standards_control
+</details>
+
 ## service-quotas
 <details>
 <summary>10% implemented</summary>
@@ -9624,7 +9709,6 @@
 - savingsplans
 - schemas
 - security-ir
-- securityhub
 - securitylake
 - serverlessrepo
 - servicecatalog
diff --git a/docs/docs/services/securityhub.rst b/docs/docs/services/securityhub.rst
new file mode 100644
index 000000000..a395350a1
--- /dev/null
+++ b/docs/docs/services/securityhub.rst
@@ -0,0 +1,112 @@
+.. _implementedservice_securityhub:
+
+.. |start-h3| raw:: html
+
+    <h3>
+
+.. |end-h3| raw:: html
+
+    </h3>
+
+===========
+securityhub
+===========
+
+.. autoclass:: moto.securityhub.models.SecurityHubBackend
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] accept_administrator_invitation
+- [ ] accept_invitation
+- [ ] batch_delete_automation_rules
+- [ ] batch_disable_standards
+- [ ] batch_enable_standards
+- [ ] batch_get_automation_rules
+- [ ] batch_get_configuration_policy_associations
+- [ ] batch_get_security_controls
+- [ ] batch_get_standards_control_associations
+- [X] batch_import_findings
+  
+        Import findings in batch to SecurityHub.
+
+        Args:
+            findings: List of finding dictionaries to import
+
+        Returns:
+            Tuple of (failed_count, success_count, failed_findings)
+        
+
+- [ ] batch_update_automation_rules
+- [ ] batch_update_findings
+- [ ] batch_update_standards_control_associations
+- [ ] create_action_target
+- [ ] create_automation_rule
+- [ ] create_configuration_policy
+- [ ] create_finding_aggregator
+- [ ] create_insight
+- [ ] create_members
+- [ ] decline_invitations
+- [ ] delete_action_target
+- [ ] delete_configuration_policy
+- [ ] delete_finding_aggregator
+- [ ] delete_insight
+- [ ] delete_invitations
+- [ ] delete_members
+- [ ] describe_action_targets
+- [ ] describe_hub
+- [ ] describe_organization_configuration
+- [ ] describe_products
+- [ ] describe_standards
+- [ ] describe_standards_controls
+- [ ] disable_import_findings_for_product
+- [ ] disable_organization_admin_account
+- [ ] disable_security_hub
+- [ ] disassociate_from_administrator_account
+- [ ] disassociate_from_master_account
+- [ ] disassociate_members
+- [ ] enable_import_findings_for_product
+- [ ] enable_organization_admin_account
+- [ ] enable_security_hub
+- [ ] get_administrator_account
+- [ ] get_configuration_policy
+- [ ] get_configuration_policy_association
+- [ ] get_enabled_standards
+- [ ] get_finding_aggregator
+- [ ] get_finding_history
+- [X] get_findings
+  
+        Returns findings based on optional filters and sort criteria.
+        
+
+- [ ] get_insight_results
+- [ ] get_insights
+- [ ] get_invitations_count
+- [ ] get_master_account
+- [ ] get_members
+- [ ] get_security_control_definition
+- [ ] invite_members
+- [ ] list_automation_rules
+- [ ] list_configuration_policies
+- [ ] list_configuration_policy_associations
+- [ ] list_enabled_products_for_import
+- [ ] list_finding_aggregators
+- [ ] list_invitations
+- [ ] list_members
+- [ ] list_organization_admin_accounts
+- [ ] list_security_control_definitions
+- [ ] list_standards_control_associations
+- [ ] list_tags_for_resource
+- [ ] start_configuration_policy_association
+- [ ] start_configuration_policy_disassociation
+- [ ] tag_resource
+- [ ] untag_resource
+- [ ] update_action_target
+- [ ] update_configuration_policy
+- [ ] update_finding_aggregator
+- [ ] update_findings
+- [ ] update_insight
+- [ ] update_organization_configuration
+- [ ] update_security_control
+- [ ] update_security_hub_configuration
+- [ ] update_standards_control
+
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 9d8abc829..213550592 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -184,6 +184,7 @@ backend_url_patterns = [
     ("scheduler", re.compile("https?://scheduler\\.(.+)\\.amazonaws\\.com")),
     ("sdb", re.compile("https?://sdb\\.(.+)\\.amazonaws\\.com")),
     ("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")),
+    ("securityhub", re.compile("https?://securityhub\\.(.+)\\.amazonaws\\.com")),
     (
         "servicediscovery",
         re.compile("https?://(data-)?servicediscovery\\.(.+)\\.amazonaws\\.com"),
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 4439041a7..0783bee9c 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -433,7 +433,7 @@ class CognitoIdpUserPool(BaseModel):
         self.groups: Dict[str, CognitoIdpGroup] = OrderedDict()
         self.users: Dict[str, CognitoIdpUser] = OrderedDict()
         self.resource_servers: Dict[str, CognitoResourceServer] = OrderedDict()
-        self.refresh_tokens: Dict[str, Optional[Tuple[str, str]]] = {}
+        self.refresh_tokens: Dict[str, Optional[Tuple[str, str, str]]] = {}
         self.access_tokens: Dict[str, Tuple[str, str]] = {}
         self.id_tokens: Dict[str, Tuple[str, str]] = {}
 
@@ -534,6 +534,7 @@ class CognitoIdpUserPool(BaseModel):
             "token_use": token_use,
             "auth_time": now,
             "exp": now + expires_in,
+            "jti": str(random.uuid4()),
         }
         username_is_email = "email" in self.extended_config.get(
             "UsernameAttributes", []
@@ -574,8 +575,14 @@ class CognitoIdpUserPool(BaseModel):
         for attribute in attributes:
             self.schema_attributes[attribute.name] = attribute
 
-    def create_id_token(self, client_id: str, username: str) -> Tuple[str, int]:
+    def create_id_token(
+        self, client_id: str, username: str, origin_jti: str
+    ) -> Tuple[str, int]:
+        """
+        :returns: (id_token, expires_in)
+        """
         extra_data = self.get_user_extra_data_by_client_id(client_id, username)
+        extra_data["origin_jti"] = origin_jti
         user = self._get_user(username)
         for attr in user.attributes:
             if attr["Name"].startswith("custom:"):
@@ -588,13 +595,24 @@ class CognitoIdpUserPool(BaseModel):
         self.id_tokens[id_token] = (client_id, username)
         return id_token, expires_in
 
-    def create_refresh_token(self, client_id: str, username: str) -> str:
+    def create_refresh_token(self, client_id: str, username: str) -> Tuple[str, str]:
+        """
+        :returns: (refresh_token, origin_jti)
+        """
         refresh_token = str(random.uuid4())
-        self.refresh_tokens[refresh_token] = (client_id, username)
-        return refresh_token
+        origin_jti = str(random.uuid4())
+        self.refresh_tokens[refresh_token] = (client_id, username, origin_jti)
+        return refresh_token, origin_jti
 
-    def create_access_token(self, client_id: str, username: str) -> Tuple[str, int]:
-        extra_data = {}
+    def create_access_token(
+        self, client_id: str, username: str, origin_jti: str
+    ) -> Tuple[str, int]:
+        """
+        :returns: (access_token, expires_in)
+        """
+        extra_data: Dict[str, Any] = {
+            "origin_jti": origin_jti,
+        }
         user = self._get_user(username)
         if len(user.groups) > 0:
             extra_data["cognito:groups"] = [group.group_name for group in user.groups]
@@ -611,12 +629,14 @@ class CognitoIdpUserPool(BaseModel):
         res = self.refresh_tokens[refresh_token]
         if res is None:
             raise NotAuthorizedError(refresh_token)
-        client_id, username = res
+        client_id, username, origin_jti = res
         if not username:
             raise NotAuthorizedError(refresh_token)
 
-        access_token, expires_in = self.create_access_token(client_id, username)
-        id_token, _ = self.create_id_token(client_id, username)
+        access_token, expires_in = self.create_access_token(
+            client_id, username, origin_jti=origin_jti
+        )
+        id_token, _ = self.create_id_token(client_id, username, origin_jti=origin_jti)
         return access_token, id_token, expires_in
 
     def get_user_extra_data_by_client_id(
@@ -640,11 +660,10 @@ class CognitoIdpUserPool(BaseModel):
         for token, token_tuple in list(self.refresh_tokens.items()):
             if token_tuple is None:
                 continue
-            _, logged_in_user = token_tuple
+            _, logged_in_user, _ = token_tuple
             if username == logged_in_user:
                 self.refresh_tokens[token] = None
-        for access_token, token_tuple in list(self.access_tokens.items()):
-            _, logged_in_user = token_tuple
+        for access_token, (_, logged_in_user) in list(self.access_tokens.items()):
             if username == logged_in_user:
                 self.access_tokens.pop(access_token)
 
@@ -1427,7 +1446,7 @@ class CognitoIdpBackend(BaseBackend):
         client: CognitoIdpUserPoolClient,
         username: str,
     ) -> Dict[str, Dict[str, Any]]:
-        refresh_token = user_pool.create_refresh_token(client.id, username)
+        refresh_token, _ = user_pool.create_refresh_token(client.id, username)
         access_token, id_token, expires_in = user_pool.create_tokens_from_refresh_token(
             refresh_token
         )
@@ -2083,11 +2102,15 @@ class CognitoIdpBackend(BaseBackend):
                     "Session": session,
                 }
 
-            access_token, expires_in = user_pool.create_access_token(
+            new_refresh_token, origin_jti = user_pool.create_refresh_token(
                 client_id, username
             )
-            id_token, _ = user_pool.create_id_token(client_id, username)
-            new_refresh_token = user_pool.create_refresh_token(client_id, username)
+            access_token, expires_in = user_pool.create_access_token(
+                client_id, username, origin_jti=origin_jti
+            )
+            id_token, _ = user_pool.create_id_token(
+                client_id, username, origin_jti=origin_jti
+            )
 
             return {
                 "AuthenticationResult": {
@@ -2107,7 +2130,7 @@ class CognitoIdpBackend(BaseBackend):
             if res is None:
                 raise NotAuthorizedError("Refresh Token has been revoked")
 
-            client_id, username = res
+            client_id, username, _ = res
             if not username:
                 raise ResourceNotFoundError(username)
 
diff --git a/moto/securityhub/__init__.py b/moto/securityhub/__init__.py
new file mode 100644
index 000000000..68a7cc517
--- /dev/null
+++ b/moto/securityhub/__init__.py
@@ -0,0 +1,1 @@
+from .models import securityhub_backends  # noqa: F401
diff --git a/moto/securityhub/exceptions.py b/moto/securityhub/exceptions.py
new file mode 100644
index 000000000..1ff50c3d8
--- /dev/null
+++ b/moto/securityhub/exceptions.py
@@ -0,0 +1,21 @@
+"""Exceptions raised by the securityhub service."""
+
+from moto.core.exceptions import JsonRESTError
+
+
+class SecurityHubClientError(JsonRESTError):
+    code = 400
+
+
+class _InvalidOperationException(SecurityHubClientError):
+    def __init__(self, error_type: str, op: str, msg: str):
+        super().__init__(
+            error_type,
+            "An error occurred (%s) when calling the %s operation: %s"
+            % (error_type, op, msg),
+        )
+
+
+class InvalidInputException(_InvalidOperationException):
+    def __init__(self, op: str, msg: str):
+        super().__init__("InvalidInputException", op, msg)
diff --git a/moto/securityhub/models.py b/moto/securityhub/models.py
new file mode 100644
index 000000000..afcd6fcae
--- /dev/null
+++ b/moto/securityhub/models.py
@@ -0,0 +1,122 @@
+"""SecurityHubBackend class with methods for supported APIs."""
+
+from typing import Any, Dict, List, Optional, Tuple
+
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.core.common_models import BaseModel
+from moto.securityhub.exceptions import InvalidInputException
+from moto.utilities.paginator import paginate
+
+
+class Finding(BaseModel):
+    def __init__(self, finding_id: str, finding_data: Dict[str, Any]):
+        self.id = finding_id
+        self.data = finding_data
+
+    def as_dict(self) -> Dict[str, Any]:
+        return self.data
+
+
+class SecurityHubBackend(BaseBackend):
+    """Implementation of SecurityHub APIs."""
+
+    PAGINATION_MODEL = {
+        "get_findings": {
+            "input_token": "next_token",
+            "limit_key": "max_results",
+            "limit_default": 100,
+            "unique_attribute": "Id",
+            "fail_on_invalid_token": True,
+        }
+    }
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+        self.findings: List[Finding] = []
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def get_findings(
+        self,
+        filters: Optional[Dict[str, Any]] = None,
+        sort_criteria: Optional[List[Dict[str, str]]] = None,
+        max_results: Optional[int] = None,
+        next_token: Optional[str] = None,
+    ) -> List[Dict[str, str]]:
+        """
+        Returns findings based on optional filters and sort criteria.
+        """
+        if max_results is not None:
+            try:
+                max_results = int(max_results)
+                if max_results < 1 or max_results > 100:
+                    raise InvalidInputException(
+                        op="GetFindings",
+                        msg="MaxResults must be a number between 1 and 100",
+                    )
+            except ValueError:
+                raise InvalidInputException(
+                    op="GetFindings", msg="MaxResults must be a number greater than 0"
+                )
+
+        findings = self.findings
+
+        # TODO: Apply filters if provided
+        # TODO: Apply sort criteria if provided
+
+        return [f.as_dict() for f in findings]
+
+    def batch_import_findings(
+        self, findings: List[Dict[str, Any]]
+    ) -> Tuple[int, int, List[Dict[str, Any]]]:
+        """
+        Import findings in batch to SecurityHub.
+
+        Args:
+            findings: List of finding dictionaries to import
+
+        Returns:
+            Tuple of (failed_count, success_count, failed_findings)
+        """
+        failed_count = 0
+        success_count = 0
+        failed_findings = []
+
+        for finding_data in findings:
+            try:
+                if (
+                    not isinstance(finding_data["Resources"], list)
+                    or len(finding_data["Resources"]) == 0
+                ):
+                    raise InvalidInputException(
+                        op="BatchImportFindings",
+                        msg="Finding must contain at least one resource in the Resources array",
+                    )
+
+                finding_id = finding_data["Id"]
+
+                existing_finding = next(
+                    (f for f in self.findings if f.id == finding_id), None
+                )
+
+                if existing_finding:
+                    existing_finding.data.update(finding_data)
+                else:
+                    new_finding = Finding(finding_id, finding_data)
+                    self.findings.append(new_finding)
+
+                success_count += 1
+
+            except Exception as e:
+                failed_count += 1
+                failed_findings.append(
+                    {
+                        "Id": finding_data.get("Id", ""),
+                        "ErrorCode": "InvalidInput",
+                        "ErrorMessage": str(e),
+                    }
+                )
+
+        return failed_count, success_count, failed_findings
+
+
+securityhub_backends = BackendDict(SecurityHubBackend, "securityhub")
diff --git a/moto/securityhub/responses.py b/moto/securityhub/responses.py
new file mode 100644
index 000000000..8ce0ce1e2
--- /dev/null
+++ b/moto/securityhub/responses.py
@@ -0,0 +1,61 @@
+"""Handles incoming securityhub requests, invokes methods, returns responses."""
+
+import json
+
+from moto.core.responses import BaseResponse
+
+from .models import SecurityHubBackend, securityhub_backends
+
+
+class SecurityHubResponse(BaseResponse):
+    def __init__(self) -> None:
+        super().__init__(service_name="securityhub")
+
+    @property
+    def securityhub_backend(self) -> SecurityHubBackend:
+        return securityhub_backends[self.current_account][self.region]
+
+    def get_findings(self) -> str:
+        filters = self._get_param("Filters")
+        sort_criteria = self._get_param("SortCriteria")
+        max_results = self._get_param("MaxResults")
+        next_token = self._get_param("NextToken")
+
+        findings, next_token = self.securityhub_backend.get_findings(
+            filters=filters,
+            sort_criteria=sort_criteria,
+            max_results=max_results,
+            next_token=next_token,
+        )
+
+        response = {"Findings": findings, "NextToken": next_token}
+        return json.dumps(response)
+
+    def batch_import_findings(self) -> str:
+        raw_body = self.body
+        if isinstance(raw_body, bytes):
+            raw_body = raw_body.decode("utf-8")
+        body = json.loads(raw_body)
+
+        findings = body.get("Findings", [])
+
+        failed_count, success_count, failed_findings = (
+            self.securityhub_backend.batch_import_findings(
+                findings=findings,
+            )
+        )
+
+        return json.dumps(
+            {
+                "FailedCount": failed_count,
+                "FailedFindings": [
+                    {
+                        "ErrorCode": finding.get("ErrorCode"),
+                        "ErrorMessage": finding.get("ErrorMessage"),
+                        "Id": finding.get("Id"),
+                    }
+                    for finding in failed_findings
+                ],
+                "SuccessCount": success_count,
+            }
+        )
diff --git a/moto/securityhub/urls.py b/moto/securityhub/urls.py
new file mode 100644
index 000000000..162e66d8a
--- /dev/null
+++ b/moto/securityhub/urls.py
@@ -0,0 +1,12 @@
+"""securityhub base URL and path."""
+
+from .responses import SecurityHubResponse
+
+url_bases = [
+    r"https?://securityhub\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/findings$": SecurityHubResponse.dispatch,
+    "{0}/findings/import$": SecurityHubResponse.dispatch,
+}
diff --git a/moto/ssm/resources/regions.json b/moto/ssm/resources/regions.json
index ed4e03673..5982ea9f1 100644
--- a/moto/ssm/resources/regions.json
+++ b/moto/ssm/resources/regions.json
@@ -1057,6 +1057,15 @@
               "marketplace": {
                 "Value": "marketplace"
               },
+              "mcs": {
+                "Value": "mcs",
+                "endpoint": {
+                  "Value": "cassandra.af-south-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS, TLS"
+                }
+              },
               "mediaconnect": {
                 "Value": "mediaconnect",
                 "endpoint": {
@@ -23460,6 +23469,15 @@
                   "Value": "HTTPS, HTTP"
                 }
               },
+              "sagemaker": {
+                "Value": "sagemaker",
+                "endpoint": {
+                  "Value": "api.sagemaker.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "sagemaker-metrics": {
                 "Value": "sagemaker-metrics",
                 "endpoint": {
@@ -23713,6 +23731,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "acm-pca": {
+                "Value": "acm-pca",
+                "endpoint": {
+                  "Value": "acm-pca.ap-southeast-7.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "https"
+                }
+              },
               "apigateway": {
                 "Value": "apigateway",
                 "endpoint": {
@@ -27542,6 +27569,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "pca-connector-ad": {
+                "Value": "pca-connector-ad",
+                "endpoint": {
+                  "Value": "pca-connector-ad.ca-west-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "pca-connector-scep": {
                 "Value": "pca-connector-scep",
                 "endpoint": {
@@ -48828,6 +48864,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "pca-connector-ad": {
+                "Value": "pca-connector-ad",
+                "endpoint": {
+                  "Value": "pca-connector-ad.il-central-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "pca-connector-scep": {
                 "Value": "pca-connector-scep",
                 "endpoint": {
@@ -52560,6 +52605,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "acm-pca": {
+                "Value": "acm-pca",
+                "endpoint": {
+                  "Value": "acm-pca.mx-central-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "https"
+                }
+              },
               "apigateway": {
                 "Value": "apigateway",
                 "endpoint": {
diff --git a/moto/ssm/resources/services.json b/moto/ssm/resources/services.json
index 282229650..15b7096c2 100644
--- a/moto/ssm/resources/services.json
+++ b/moto/ssm/resources/services.json
@@ -1056,6 +1056,15 @@
                   "Value": "https"
                 }
               },
+              "ap-southeast-7": {
+                "Value": "ap-southeast-7",
+                "endpoint": {
+                  "Value": "acm-pca.ap-southeast-7.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "https"
+                }
+              },
               "ca-central-1": {
                 "Value": "ca-central-1",
                 "endpoint": {
@@ -1191,6 +1200,15 @@
                   "Value": "https"
                 }
               },
+              "mx-central-1": {
+                "Value": "mx-central-1",
+                "endpoint": {
+                  "Value": "acm-pca.mx-central-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "https"
+                }
+              },
               "sa-east-1": {
                 "Value": "sa-east-1",
                 "endpoint": {
@@ -41858,6 +41876,15 @@
               "Value": "https://aws.amazon.com/keyspaces/"
             },
             "regions": {
+              "af-south-1": {
+                "Value": "af-south-1",
+                "endpoint": {
+                  "Value": "cassandra.af-south-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS, TLS"
+                }
+              },
               "ap-east-1": {
                 "Value": "ap-east-1",
                 "endpoint": {
@@ -48625,6 +48652,10 @@
             }
           },
           "pca-connector-ad": {
+            "Value": "pca-connector-ad",
+            "longName": {
+              "Value": "Private CA Connector for Active Directory"
+            },
             "regions": {
               "af-south-1": {
                 "Value": "af-south-1",
@@ -48734,6 +48765,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "ca-west-1": {
+                "Value": "ca-west-1",
+                "endpoint": {
+                  "Value": "pca-connector-ad.ca-west-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "eu-central-1": {
                 "Value": "eu-central-1",
                 "endpoint": {
@@ -48806,6 +48846,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "il-central-1": {
+                "Value": "il-central-1",
+                "endpoint": {
+                  "Value": "pca-connector-ad.il-central-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "me-central-1": {
                 "Value": "me-central-1",
                 "endpoint": {
@@ -57573,6 +57622,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "ap-southeast-5": {
+                "Value": "ap-southeast-5",
+                "endpoint": {
+                  "Value": "api.sagemaker.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "ca-central-1": {
                 "Value": "ca-central-1",
                 "endpoint": {


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 17c303ace..6795c7062 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@2654679fe7f7c29875c669398a8ec0791b8a64a1
+        uses: ruby/setup-ruby@d781c1b4ed31764801bfae177617bb0446f5ef8d
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 06028c141..1a6cfb57d 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -9,6 +9,7 @@ import re
 import time
 import uuid
 from unittest import SkipTest, mock
+from uuid import UUID
 
 import boto3
 import pycognito
@@ -1547,6 +1548,126 @@ def test_group_in_access_token(user_pool=None, user_pool_client=None):
     assert payload.claims["cognito:groups"] == [group_name]
 
 
+@pytest.mark.aws_verified
+@cognitoidp_aws_verified(
+    generate_secret=True, explicit_auth_flows=["ADMIN_NO_SRP_AUTH"]
+)
+def test_jti_in_tokens(user_pool=None, user_pool_client=None):
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    username = str(uuid.uuid4())
+    temporary_password = "P2$Sword"
+    new_password = "P2$Sword"
+    user_pool_id = user_pool["UserPool"]["Id"]
+    user_attribute_value = str(uuid.uuid4())
+    client_id = user_pool_client["UserPoolClient"]["ClientId"]
+    client_secret = user_pool_client["UserPoolClient"]["ClientSecret"]
+    secret_hash = pycognito.aws_srp.AWSSRP.get_secret_hash(
+        username=username, client_id=client_id, client_secret=client_secret
+    )
+
+    conn.admin_create_user(
+        UserPoolId=user_pool_id,
+        Username=username,
+        TemporaryPassword=temporary_password,
+        UserAttributes=[{"Name": "given_name", "Value": user_attribute_value}],
+    )
+
+    result = conn.admin_initiate_auth(
+        UserPoolId=user_pool_id,
+        ClientId=client_id,
+        AuthFlow="ADMIN_NO_SRP_AUTH",
+        AuthParameters={
+            "USERNAME": username,
+            "PASSWORD": temporary_password,
+            "SECRET_HASH": secret_hash,
+        },
+    )
+
+    # This sets a new password and logs the user in (creates tokens)
+    initial_login = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
+        Session=result["Session"],
+        ClientId=client_id,
+        ChallengeName="NEW_PASSWORD_REQUIRED",
+        ChallengeResponses={
+            "USERNAME": username,
+            "NEW_PASSWORD": new_password,
+            "SECRET_HASH": secret_hash,
+        },
+    )["AuthenticationResult"]
+
+    initial_access_claims = get_jwt_payload(initial_login["AccessToken"]).claims
+
+    initial_id_claims = get_jwt_payload(initial_login["IdToken"]).claims
+
+    # origin_jti
+    # A token-revocation identifier associated with your user's refresh token.
+    # Should be the same for all tokens, for a single session
+    assert UUID(initial_access_claims["origin_jti"])
+    assert initial_access_claims["origin_jti"] == initial_id_claims["origin_jti"]
+
+    # jti
+    # The unique identifier of the JWT.
+    # Should be unique for every token
+    assert UUID(initial_access_claims["jti"])
+    assert UUID(initial_id_claims["jti"])
+    assert initial_access_claims["jti"] != initial_id_claims["jti"]
+
+    # refresh current session
+    refreshed_tokens = conn.initiate_auth(
+        ClientId=client_id,
+        AuthFlow="REFRESH_TOKEN",
+        AuthParameters={
+            "SECRET_HASH": secret_hash,
+            "REFRESH_TOKEN": initial_login["RefreshToken"],
+        },
+    )["AuthenticationResult"]
+    refresh_access_claims = get_jwt_payload(refreshed_tokens["AccessToken"]).claims
+
+    refresh_id_claims = get_jwt_payload(refreshed_tokens["IdToken"]).claims
+
+    assert initial_access_claims["origin_jti"] == refresh_access_claims["origin_jti"]
+    assert refresh_access_claims["origin_jti"] == refresh_id_claims["origin_jti"]
+
+    assert initial_access_claims["jti"] != refresh_access_claims["jti"]
+    assert refresh_access_claims["jti"] != refresh_id_claims["jti"]
+
+    # new session
+    aws_srp = pycognito.aws_srp.AWSSRP(
+        username=username,
+        password=new_password,
+        pool_id=user_pool_id,
+        client_id=client_id,
+        client_secret=client_secret,
+        client=conn,
+    )
+    auth_params = aws_srp.get_auth_params()
+
+    result = conn.initiate_auth(
+        ClientId=client_id,
+        AuthFlow="USER_SRP_AUTH",
+        AuthParameters=auth_params,
+    )
+
+    challenge_response = aws_srp.process_challenge(
+        result["ChallengeParameters"], auth_params
+    )
+    new_session = conn.respond_to_auth_challenge(
+        ClientId=client_id,
+        ChallengeName=result["ChallengeName"],
+        ChallengeResponses=challenge_response,
+    )["AuthenticationResult"]
+    new_access_claims = get_jwt_payload(new_session["AccessToken"]).claims
+
+    new_id_claims = get_jwt_payload(new_session["IdToken"]).claims
+
+    assert initial_access_claims["origin_jti"] != new_access_claims["origin_jti"]
+    assert new_access_claims["origin_jti"] == new_id_claims["origin_jti"]
+
+    assert new_access_claims["jti"] != new_id_claims["jti"]
+
+
 @mock_aws
 def test_other_attributes_in_id_token():
     conn = boto3.client("cognito-idp", "us-west-2")
diff --git a/tests/test_securityhub/__init__.py b/tests/test_securityhub/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_securityhub/test_securityhub.py b/tests/test_securityhub/test_securityhub.py
new file mode 100644
index 000000000..3a95c07a4
--- /dev/null
+++ b/tests/test_securityhub/test_securityhub.py
@@ -0,0 +1,156 @@
+"""Unit tests for securityhub-supported APIs."""
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+from moto.core import DEFAULT_ACCOUNT_ID
+
+
+@mock_aws
+def test_get_findings():
+    client = boto3.client("securityhub", region_name="us-east-1")
+
+    test_finding = {
+        "AwsAccountId": DEFAULT_ACCOUNT_ID,
+        "CreatedAt": "2024-01-01T00:00:00.001Z",
+        "UpdatedAt": "2024-01-01T00:00:00.000Z",
+        "Description": "Test finding description",
+        "GeneratorId": "test-generator",
+        "Id": "test-finding-001",
+        "ProductArn": f"arn:aws:securityhub:{client.meta.region_name}:{DEFAULT_ACCOUNT_ID}:product/{DEFAULT_ACCOUNT_ID}/default",
+        "Resources": [{"Id": "test-resource", "Type": "AwsEc2Instance"}],
+        "SchemaVersion": "2018-10-08",
+        "Severity": {"Label": "HIGH"},
+        "Title": "Test Finding",
+        "Types": ["Software and Configuration Checks"],
+    }
+
+    import_response = client.batch_import_findings(Findings=[test_finding])
+    assert import_response["SuccessCount"] == 1
+
+    response = client.get_findings()
+
+    assert "Findings" in response
+    assert isinstance(response["Findings"], list)
+    assert len(response["Findings"]) == 1
+    finding = response["Findings"][0]
+    assert finding["Id"] == "test-finding-001"
+    assert finding["SchemaVersion"] == "2018-10-08"
+
+
+@mock_aws
+def test_batch_import_findings():
+    client = boto3.client("securityhub", region_name="us-east-2")
+
+    valid_finding = {
+        "AwsAccountId": DEFAULT_ACCOUNT_ID,
+        "CreatedAt": "2024-01-01T00:00:00.000Z",
+        "UpdatedAt": "2024-01-01T00:00:00.000Z",
+        "Description": "Test finding description",
+        "GeneratorId": "test-generator",
+        "Id": "test-finding-001",
+        "ProductArn": f"arn:aws:securityhub:{client.meta.region_name}:{DEFAULT_ACCOUNT_ID}:product/{DEFAULT_ACCOUNT_ID}/default",
+        "Resources": [{"Id": "test-resource", "Type": "AwsEc2Instance"}],
+        "SchemaVersion": "2018-10-08",
+        "Severity": {"Label": "HIGH"},
+        "Title": "Test Finding",
+        "Types": ["Software and Configuration Checks"],
+    }
+
+    response = client.batch_import_findings(Findings=[valid_finding])
+    assert response["SuccessCount"] == 1
+    assert response["FailedCount"] == 0
+    assert response["FailedFindings"] == []
+
+    invalid_finding = valid_finding.copy()
+    invalid_finding["Id"] = "test-finding-002"
+    invalid_finding["Severity"]["Label"] = "INVALID_LABEL"
+
+    response = client.batch_import_findings(Findings=[invalid_finding])
+
+    assert response["SuccessCount"] == 1
+    assert response["FailedCount"] == 0
+    assert len(response["FailedFindings"]) == 0
+
+
+@mock_aws
+def test_get_findings_invalid_parameters():
+    client = boto3.client("securityhub", region_name="us-east-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.get_findings(MaxResults=101)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInputException"
+    assert "MaxResults must be a number between 1 and 100" in err["Message"]
+
+
+@mock_aws
+def test_batch_import_multiple_findings():
+    client = boto3.client("securityhub", region_name="us-east-1")
+
+    findings = [
+        {
+            "AwsAccountId": DEFAULT_ACCOUNT_ID,
+            "CreatedAt": "2024-01-01T00:00:00.000Z",
+            "UpdatedAt": "2024-01-01T00:00:00.000Z",
+            "Description": f"Test finding description {i}",
+            "GeneratorId": "test-generator",
+            "Id": f"test-finding-{i:03d}",
+            "ProductArn": f"arn:aws:securityhub:{client.meta.region_name}:{DEFAULT_ACCOUNT_ID}:product/{DEFAULT_ACCOUNT_ID}/default",
+            "Resources": [{"Id": f"test-resource-{i}", "Type": "AwsEc2Instance"}],
+            "SchemaVersion": "2018-10-08",
+            "Severity": {"Label": "HIGH"},
+            "Title": f"Test Finding {i}",
+            "Types": ["Software and Configuration Checks"],
+        }
+        for i in range(1, 4)
+    ]
+
+    import_response = client.batch_import_findings(Findings=findings)
+    assert import_response["SuccessCount"] == 3
+    assert import_response["FailedCount"] == 0
+    assert import_response["FailedFindings"] == []
+
+    get_response = client.get_findings()
+    assert "Findings" in get_response
+    assert isinstance(get_response["Findings"], list)
+    assert len(get_response["Findings"]) == 3
+
+    imported_ids = {finding["Id"] for finding in get_response["Findings"]}
+    expected_ids = {f"test-finding-{i:03d}" for i in range(1, 4)}
+    assert imported_ids == expected_ids
+
+
+@mock_aws
+def test_get_findings_max_results():
+    client = boto3.client("securityhub", region_name="us-east-1")
+
+    findings = [
+        {
+            "AwsAccountId": DEFAULT_ACCOUNT_ID,
+            "CreatedAt": "2024-01-01T00:00:00.000Z",
+            "UpdatedAt": "2024-01-01T00:00:00.000Z",
+            "Description": f"Test finding description {i}",
+            "GeneratorId": "test-generator",
+            "Id": f"test-finding-{i:03d}",
+            "ProductArn": f"arn:aws:securityhub:{client.meta.region_name}:{DEFAULT_ACCOUNT_ID}:product/{DEFAULT_ACCOUNT_ID}/default",
+            "Resources": [{"Id": f"test-resource-{i}", "Type": "AwsEc2Instance"}],
+            "SchemaVersion": "2018-10-08",
+            "Severity": {"Label": "HIGH"},
+            "Title": f"Test Finding {i}",
+            "Types": ["Software and Configuration Checks"],
+        }
+        for i in range(1, 4)
+    ]
+
+    import_response = client.batch_import_findings(Findings=findings)
+    assert import_response["SuccessCount"] == 3
+
+    get_response = client.get_findings(MaxResults=1)
+    assert "Findings" in get_response
+    assert isinstance(get_response["Findings"], list)
+    assert len(get_response["Findings"]) == 1
+    assert "NextToken" in get_response