Details
| ID | OktaApiModule |
|---|---|
| Language | python |
| From Version | 5.0.0 |
| Docker Image | demisto/crypto:1.0.0.111961 |
| Tags | infra server |
README
Common Okta client code that will be appended into each Okta based integration when it’s deployed
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Tags | infra, server |
| Cortex XSOAR Version | 5.5.0 |
Inputs
There are no inputs for this script.
Outputs
There are no outputs for this script.
import uuid from datetime import datetime, timedelta from enum import Enum import jwt from CommonServerPython import * TOKEN_EXPIRATION_TIME = 60 # In minutes. This value must be a maximum of only an hour (according to Okta's documentation). TOKEN_RENEWAL_TIME_LIMIT = 60 # In seconds. The minimum time before the token expires to renew it. class JWTAlgorithm(Enum): RS256 = "RS256" RS384 = "RS384" RS512 = "RS512" ES256 = "ES256" ES384 = "ES384" ES512 = "ES512" class AuthType(Enum): API_TOKEN = 1 OAUTH = 2 NO_AUTH = 3 class OktaClient(BaseClient): def __init__( self, auth_type: AuthType = AuthType.API_TOKEN, api_token: str | None = None, client_id: str | None = None, scopes: list[str] | None = None, private_key: str | None = None, jwt_algorithm: JWTAlgorithm | None = None, key_id: str | None = None, *args, **kwargs, ): """ Args: auth_type (AuthType, optional): The type of authentication to use. api_token (str | None, optional): API token for authentication (required if 'auth_type' is AuthType.API_TOKEN). client_id (str | None, optional): Client ID for OAuth authentication (required if 'auth_type' is AuthType.OAUTH). scopes (list[str] | None, optional): A list of scopes to request for the token (required if 'auth_type' is AuthType.OAUTH). private_key (str | None, optional): Private key for OAuth authentication (required if 'auth_type' is AuthType.OAUTH). jwt_algorithm (str | None, optional): The algorithm to use for JWT signing (required if 'auth_type' is AuthType.OAUTH). """ super().__init__(*args, **kwargs) self.auth_type = auth_type self.api_token = api_token self.client_id = client_id self.scopes = scopes self.jwt_algorithm = jwt_algorithm self.private_key = private_key self.key_id = key_id self.request_metadata: dict = {} missing_required_params = [] if self.auth_type == AuthType.API_TOKEN and not api_token: raise ValueError("API token is missing") if self.auth_type == AuthType.OAUTH: if not self.client_id: missing_required_params.append("Client ID") if not self.scopes: missing_required_params.append("Scopes") if not self.jwt_algorithm: missing_required_params.append("JWT algorithm") if not self.private_key: missing_required_params.append("Private key") if missing_required_params: raise ValueError(f'Required OAuth parameters are missing: {", ".join(missing_required_params)}') def assign_app_role(self, client_id: str, role: str, auth_type: AuthType) -> dict: """ Assign a role to a client application. Args: client_id (str): The ID of the client application. role (str): The role to assign to the client application. auth_type (AuthType, optional): Authentication type to use for the request. Defaults to AuthType.API_TOKEN. Returns: dict: The response from the API. """ return self.http_request( auth_type=auth_type, url_suffix=f"/oauth2/v1/clients/{client_id}/roles", method="POST", json_data={ "type": role, }, ) def generate_jwt_token(self, url: str) -> str: """ Generate a JWT token to use for OAuth authentication. Args: url (str): The URL to use for the JWT token (for the 'aud' claim). Returns: str: The JWT token. """ current_time = datetime.utcnow() expiration_time = current_time + timedelta(minutes=TOKEN_EXPIRATION_TIME) payload = { "aud": url, "iat": int((current_time - datetime(1970, 1, 1)).total_seconds()), "exp": int((expiration_time - datetime(1970, 1, 1)).total_seconds()), "iss": self.client_id, "sub": self.client_id, "jti": str(uuid.uuid4()), } headers = {} if self.key_id: headers["kid"] = self.key_id return jwt.encode( payload=payload, key=self.private_key, # type: ignore[arg-type] algorithm=self.jwt_algorithm.value, # type: ignore[union-attr] headers=headers, ) def generate_oauth_token(self, scopes: list[str]) -> dict: """ Generate an OAuth token to use for authentication. Args: scopes (list[str]): A list of scopes to request for the token. Returns: dict: The response from the API. """ auth_url = self._base_url + "/oauth2/v1/token" jwt_token = self.generate_jwt_token(url=auth_url) return self.http_request( auth_type=AuthType.NO_AUTH, full_url=auth_url, method="POST", headers={ "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", }, data={ "grant_type": "client_credentials", "scope": " ".join(scopes), "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": jwt_token, }, ) def get_token(self): """ Get an OAuth token for authentication. If there isn't an existing one, or the existing one is expired, a new one will be generated. """ expiration_time_format = "%Y-%m-%dT%H:%M:%S" integration_context = get_integration_context() token = integration_context.get("token") if token: if "token_expiration" not in integration_context: raise ValueError("Token expiration data must be assigned along with the token.") token_expiration = datetime.strptime(integration_context["token_expiration"], expiration_time_format) if datetime.utcnow() + timedelta(seconds=TOKEN_RENEWAL_TIME_LIMIT) < token_expiration: return token demisto.debug("An existing token was found, but expired. A new token will be generated.") else: demisto.debug("No existing token was found. A new token will be generated.") token_generation_response = self.generate_oauth_token(scopes=self.scopes) # type: ignore[arg-type] token: str = token_generation_response["access_token"] expires_in: int = token_generation_response["expires_in"] token_expiration = datetime.utcnow() + timedelta(seconds=expires_in) integration_context["token"] = token integration_context["token_expiration"] = token_expiration.strftime(expiration_time_format) set_integration_context(integration_context) demisto.debug(f"New token generated. Expiration time: {token_expiration}") return token @staticmethod def get_rate_limit_context(headers) -> dict: """Takes the response headers and returns the rate limit data to the context Args: headers: The response headers (can be dict or CaseInsensitiveDict) Returns: dict: The rate limit data to the context """ rate_limit_context = { "x-rate-limit-limit": headers["x-rate-limit-limit"], "x-rate-limit-remaining": headers["x-rate-limit-remaining"], "x-rate-limit-reset": headers["x-rate-limit-reset"], } return rate_limit_context def parse_response_headers(self, raw_response: requests.Response): """deconstruct the raw response to headers and content""" headers = raw_response.headers self.request_metadata = self.get_rate_limit_context(headers) def http_request(self, auth_type: AuthType | None = None, resp_type: str = "json", **kwargs): """ Override BaseClient._http_request() to automatically add authentication headers. Args: auth_type (AuthType | None): Type of authentication to use for the request. If not provided, 'self.auth_type' will be used. """ auth_type = auth_type if auth_type is not None else self.auth_type auth_headers = {} if auth_type == AuthType.OAUTH: auth_headers["Authorization"] = f"Bearer {self.get_token()}" elif auth_type == AuthType.API_TOKEN: auth_headers["Authorization"] = f"SSWS {self.api_token}" original_headers = kwargs.get("headers") or self._headers or {} kwargs["headers"] = {**auth_headers, **original_headers} response = self._http_request(resp_type="response", **kwargs) self.parse_response_headers(response) resp_type = resp_type.lower() try: if resp_type == "json": return response.json() if resp_type == "text": return response.text if resp_type == "content": return response.content if resp_type == "xml": ET.fromstring(response.text) if resp_type == "response": return response return response except ValueError as exception: raise DemistoException( f"Failed to parse {resp_type} object from response: {response.content}", # type: ignore[str-bytes-safe] exception, response, ) def reset_integration_context(): """ Reset the integration context. """ integration_context = get_integration_context() integration_context["token"] = "XXX" set_integration_context({}) demisto.debug(f"Integration context reset successfully.\nIntegration context before reset: {integration_context=}")