Skip to content

Commit

Permalink
Microsoft Graph Mail Single User - Support Update of Refresh token fr…
Browse files Browse the repository at this point in the history
…om Integration Parameters (demisto#24347)

* Reset auth command solution

* Added second try of auth with the oproxy using the refresh token from the integration parameters

* Change refresh_token_param optional

* Fixed position of transferred refresh_token_param

* - Added RN
- Fixed README.md

* Update RN

* Added a change in MicrosoftGraphGroups to trigger tests with both oproxy and self deployed instances

* Added unit tests

* Added the refresh_token_param logic to AzureLogAnalytics as well

* Reverted unnecessary change in AzureLogAnalytics unit-tests

* Added the refresh_token_param logic to MicrosoftManagementActivity.py as well

* Updated the docker images tags

* Updated the RN files

* Improved comments

* Reverted the change in MicrosoftGraphMail.py

* Updated the RN of all the packs that use the MicrosoftAPIModule

* Added Oproxy to the known_words.txt

* Improved logs

* Updated the docker images tags

---------

Co-authored-by: ilaner <88267954+ilaner@users.noreply.github.com>
  • Loading branch information
ShacharKidor and ilaner authored Feb 12, 2023
1 parent 665d6ba commit ac6a7d5
Show file tree
Hide file tree
Showing 76 changed files with 361 additions and 70 deletions.
101 changes: 76 additions & 25 deletions Packs/ApiModules/Scripts/MicrosoftApiModule/MicrosoftApiModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(self, tenant_id: str = '',
token_retrieval_url: str = '{endpoint}/{tenant_id}/oauth2/v2.0/token',
app_name: str = '',
refresh_token: str = '',
refresh_token_param: Optional[str] = '',
auth_code: str = '',
scope: str = '{graph_endpoint}/.default',
grant_type: str = CLIENT_CREDENTIALS,
Expand All @@ -95,6 +96,8 @@ def __init__(self, tenant_id: str = '',
auth_id: If self deployed it's the client id, otherwise (oproxy) it's the auth id and may also
contain the token url
enc_key: If self deployed it's the client secret, otherwise (oproxy) it's the encryption key
refresh_token: The current used refresh token.
refresh_token_param: The refresh token from the integration's parameters (i.e instance configuration).
scope: The scope of the application (only if self deployed)
resource: The resource of the application (only if self deployed)
multi_resource: Where or not module uses a multiple resources (self-deployed, auth_code grant type only)
Expand Down Expand Up @@ -126,6 +129,7 @@ def __init__(self, tenant_id: str = '',
self.enc_key = enc_key
self.tenant_id = tenant_id
self.refresh_token = refresh_token
self.refresh_token_param = refresh_token_param

else:
self.token_retrieval_url = token_retrieval_url.format(tenant_id=tenant_id,
Expand Down Expand Up @@ -329,18 +333,46 @@ def get_access_token(self, resource: str = '', scope: Optional[str] = None) -> s

return access_token

def _oproxy_authorize(self, resource: str = '', scope: Optional[str] = None) -> Tuple[str, int, str]:
def _raise_authentication_error(self, oproxy_response: requests.Response):
"""
Gets a token by authorizing with oproxy.
Raises an exception for authentication error with the Oproxy server.
Args:
oproxy_response: Raw response from the Oproxy server to parse.
"""
msg = 'Error in authentication. Try checking the credentials you entered.'
try:
demisto.info('Authentication failure from server: {} {} {}'.format(
oproxy_response.status_code, oproxy_response.reason, oproxy_response.text))
err_response = oproxy_response.json()
server_msg = err_response.get('message')
if not server_msg:
title = err_response.get('title')
detail = err_response.get('detail')
if title:
server_msg = f'{title}. {detail}'
elif detail:
server_msg = detail
if server_msg:
msg += ' Server message: {}'.format(server_msg)
except Exception as ex:
demisto.error('Failed parsing error response - Exception: {}'.format(ex))
raise Exception(msg)

def _oproxy_authorize_build_request(self, headers: Dict[str, str], content: str,
scope: Optional[str] = None, resource: str = ''
) -> requests.Response:
"""
Build the Post request sent to the Oproxy server.
Args:
headers: The headers of the request.
content: The content for the request (usually contains the refresh token).
scope: A scope to add to the request. Do not use it.
resource: Resource to get.
Returns:
tuple: An access token, its expiry and refresh token.
Returns: The response from the Oproxy server.
"""
content = self.refresh_token or self.tenant_id
headers = self._add_info_headers()
oproxy_response = requests.post(
return requests.post(
self.token_retrieval_url,
headers=headers,
json={
Expand All @@ -353,25 +385,44 @@ def _oproxy_authorize(self, resource: str = '', scope: Optional[str] = None) ->
verify=self.verify
)

def _oproxy_authorize(self, resource: str = '', scope: Optional[str] = None) -> Tuple[str, int, str]:
"""
Gets a token by authorizing with oproxy.
Args:
scope: A scope to add to the request. Do not use it.
resource: Resource to get.
Returns:
tuple: An access token, its expiry and refresh token.
"""
content = self.refresh_token or self.tenant_id
headers = self._add_info_headers()
oproxy_response = self._oproxy_authorize_build_request(headers, content, scope, resource)

if not oproxy_response.ok:
msg = 'Error in authentication. Try checking the credentials you entered.'
try:
demisto.info('Authentication failure from server: {} {} {}'.format(
oproxy_response.status_code, oproxy_response.reason, oproxy_response.text))
err_response = oproxy_response.json()
server_msg = err_response.get('message')
if not server_msg:
title = err_response.get('title')
detail = err_response.get('detail')
if title:
server_msg = f'{title}. {detail}'
elif detail:
server_msg = detail
if server_msg:
msg += ' Server message: {}'.format(server_msg)
except Exception as ex:
demisto.error('Failed parsing error response - Exception: {}'.format(ex))
raise Exception(msg)
# Try to send request to the Oproxy server with the refresh token from the integration parameters
# (instance configuration).
# Relevant for cases where the user re-generated his credentials therefore the refresh token was updated.
if self.refresh_token_param:
demisto.error('Error in authentication: Oproxy server returned error, perform a second attempt'
' authorizing with the Oproxy, this time using the refresh token from the integration'
' parameters (instance configuration).')
content = self.refresh_token_param
oproxy_second_try_response = self._oproxy_authorize_build_request(headers, content, scope, resource)

if not oproxy_second_try_response.ok:
demisto.error('Authentication failure from server (second attempt - using refresh token from the'
' integration parameters: {} {} {}'.format(oproxy_second_try_response.status_code,
oproxy_second_try_response.reason,
oproxy_second_try_response.text))
self._raise_authentication_error(oproxy_response)

else: # Second try succeeded
oproxy_response = oproxy_second_try_response

else: # no refresh token for a second auth try
self._raise_authentication_error(oproxy_response)

# Oproxy authentication succeeded
try:
gcloud_function_exec_id = oproxy_response.headers.get('Function-Execution-Id')
demisto.info(f'Google Cloud Function Execution ID: {gcloud_function_exec_id}')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
TOKEN = 'dummy_token'
TENANT = 'dummy_tenant'
REFRESH_TOKEN = 'dummy_refresh'
REFRESH_TOKEN_PARAM = 'dummy_refresh_token_param'
AUTH_ID = 'dummy_auth_id'
ENC_KEY = 'dummy_enc_key'
TOKEN_URL = 'mock://dummy_url'
Expand Down Expand Up @@ -49,15 +50,17 @@ def oproxy_client_multi_resource():


def oproxy_client_refresh():
refresh_token = REFRESH_TOKEN
refresh_token = REFRESH_TOKEN # represents the refresh token from the integration context
refresh_token_param = REFRESH_TOKEN_PARAM # represents the token from the current instance config
auth_id = f'{AUTH_ID}@{TOKEN_URL}'
enc_key = ENC_KEY
app_name = APP_NAME
base_url = BASE_URL
ok_codes = OK_CODES

return MicrosoftClient(self_deployed=False, auth_id=auth_id, enc_key=enc_key, app_name=app_name,
refresh_token=refresh_token, base_url=base_url, verify=True, proxy=False, ok_codes=ok_codes)
refresh_token=refresh_token, base_url=base_url, verify=True, proxy=False, ok_codes=ok_codes,
refresh_token_param=refresh_token_param)


def self_deployed_client():
Expand Down Expand Up @@ -281,6 +284,52 @@ def get_encrypted(content, key):
assert req_res == res


def test_oproxy_auth_first_attempt_failed(mocker, requests_mock):
"""
This test checks the 'two attempts logic' of the authentication with the oproxy server.
'Two attempts logic' - In general we send to the oproxy server a refresh token that was saved in the integration
context, If for some reason the authentication request was failed, we will perform a second auth attempt in which
we will send the refresh token from the integration parameters - i.e the token is currently configured in the
instance.
In the test, we simulate a case where the oproxy server returns an error when we send an auth request, in this case
the 'Two attempts logic' should occur.
Given:
- A client generated with a refresh_token and a refresh_token_param (represents the token from the integration
parameters - i.e current instance config).
- An error mock response for the request post command to the oproxy server.
When:
- running the client._oproxy_authorize() function
Then:
- Verify that the client._oproxy_authorize() function called twice: first attempt with the refresh_token,
and second attempt with the refresh_token_param.
- Verify that an exception with the expected error message was raised.
"""

# Initialize Client
client = oproxy_client_refresh()

# Set Mockers
def get_encrypted(content, key):
return content + key
mocker.patch.object(demisto, 'error')
mocker.patch.object(client, '_add_info_headers')
mocker.patch.object(client, 'get_encrypted', side_effect=get_encrypted)
post_req_mock = requests_mock._adapter.register_uri('POST',
TOKEN_URL,
json={'error': 'Permission Denied'},
status_code=400)

# Verify results
with pytest.raises(Exception) as err:
client._oproxy_authorize()
assert post_req_mock.call_count == 2
assert REFRESH_TOKEN in post_req_mock.request_history[0].text
assert REFRESH_TOKEN_PARAM in post_req_mock.request_history[1].text
assert err.value.args[0] == 'Error in authentication. Try checking the credentials you entered.'


def test_self_deployed_request(requests_mock):
import urllib
# Set
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureActiveDirectory/ReleaseNotes/1_3_7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Active Directory Identity Protection (Deprecated)
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureActiveDirectory/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Deprecated. Use Microsoft Graph Identity and Access instead.",
"support": "xsoar",
"hidden": true,
"currentVersion": "1.3.6",
"currentVersion": "1.3.7",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureCompute/ReleaseNotes/1_2_3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Compute v2
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureCompute/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Compute",
"description": "Create and Manage Azure Virtual Machines",
"support": "xsoar",
"currentVersion": "1.2.2",
"currentVersion": "1.2.3",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureDataExplorer/ReleaseNotes/1_2_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Data Explorer
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureDataExplorer/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Data Explorer",
"description": "Use Azure Data Explorer integration to collect and analyze data inside clusters of Azure Data Explorer and manage search queries.",
"support": "xsoar",
"currentVersion": "1.2.7",
"currentVersion": "1.2.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureDevOps/ReleaseNotes/1_2_9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### AzureDevOps
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureDevOps/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "AzureDevOps",
"description": "Create and manage Git repositories in Azure DevOps Services.",
"support": "xsoar",
"currentVersion": "1.2.8",
"currentVersion": "1.2.9",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureFirewall/ReleaseNotes/1_1_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Firewall
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureFirewall/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Firewall",
"description": "Azure Firewall is a cloud-native and intelligent network firewall security service that provides breed threat protection for cloud workloads running in Azure.It's a fully stateful, firewall as a service with built-in high availability and unrestricted cloud scalability.",
"support": "xsoar",
"currentVersion": "1.1.7",
"currentVersion": "1.1.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureKeyVault/ReleaseNotes/1_1_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Key Vault
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureKeyVault/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Key Vault",
"description": "Use Key Vault to safeguard and manage cryptographic keys and secrets used by cloud applications and services.",
"support": "xsoar",
"currentVersion": "1.1.7",
"currentVersion": "1.1.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureKubernetesServices/ReleaseNotes/1_1_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Kubernetes Services
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureKubernetesServices/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Kubernetes Services",
"description": "Deploy and manage containerized applications with a fully managed Kubernetes service.",
"support": "xsoar",
"currentVersion": "1.1.7",
"currentVersion": "1.1.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ def __init__(self, self_deployed, refresh_token, auth_and_token_url, enc_key, re
private_key, client_credentials, managed_identities_client_id=None):

tenant_id = refresh_token if self_deployed else ''
# MicrosoftClient gets refresh_token_param as well as refresh_token which is the current refresh token from the
# integration context (if exists) so It will be possible to manually update the refresh token param for an
# existing integration instance.
refresh_token_param = refresh_token
refresh_token = get_integration_context().get('current_refresh_token') or refresh_token
base_url = f'https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/' \
f'{resource_group_name}/providers/Microsoft.OperationalInsights/workspaces/{workspace_name}'
self.ms_client = MicrosoftClient(
self_deployed=self_deployed,
auth_id=auth_and_token_url, # client_id for client credential
refresh_token=refresh_token,
refresh_token_param=refresh_token_param,
enc_key=enc_key, # client_secret for client credential
redirect_uri=redirect_uri,
token_retrieval_url='https://login.microsoftonline.com/{tenant_id}/oauth2/token',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ script:
description: Deletes a specified saved search in the Log Analytics workspace.
execution: false
name: azure-log-analytics-delete-saved-search
dockerimage: demisto/crypto:1.0.0.47103
dockerimage: demisto/crypto:1.0.0.47970
feed: false
isfetch: false
longRunning: false
Expand Down
6 changes: 6 additions & 0 deletions Packs/AzureLogAnalytics/ReleaseNotes/1_1_7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

#### Integrations
##### Azure Log Analytics
- Fixed an issue where the integration uses the **Token** (refresh_token) integration parameter only for the first run of the integration and ignores it in subsequent runs.
- Improved implementation of the authorization process with the Oproxy server.
- Updated the Docker image to: *demisto/crypto:1.0.0.47970*.
2 changes: 1 addition & 1 deletion Packs/AzureLogAnalytics/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Log Analytics",
"description": "Log Analytics is a service that helps you collect and analyze data generated by resources in your cloud and on-premises environments.",
"support": "xsoar",
"currentVersion": "1.1.6",
"currentVersion": "1.1.7",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureNetworkSecurityGroups/ReleaseNotes/1_2_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Network Security Groups
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureNetworkSecurityGroups/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Network Security Groups",
"description": "Azure Network Security Groups are used to filter network traffic to and from Azure resources in an Azure virtual network",
"support": "xsoar",
"currentVersion": "1.2.7",
"currentVersion": "1.2.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureRiskyUsers/ReleaseNotes/1_1_8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure Risky Users
- Improved implementation of the authorization process with the Oproxy server.
2 changes: 1 addition & 1 deletion Packs/AzureRiskyUsers/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Azure Risky Users",
"description": "Azure Risky Users provides access to all at-risk users and risk detections in Azure AD environment.",
"support": "xsoar",
"currentVersion": "1.1.7",
"currentVersion": "1.1.8",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
4 changes: 4 additions & 0 deletions Packs/AzureSQLManagement/ReleaseNotes/1_1_9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#### Integrations
##### Azure SQL Management (Beta)
- Improved implementation of the authorization process with the Oproxy server.
Loading

0 comments on commit ac6a7d5

Please sign in to comment.