AtlassianApiModule
Common Atlassian OAuth code for Jira and Confluence integrations.
python · ApiModules
Source
"""Atlassian API Module - Shared OAuth 2.0 functionality for Atlassian products (Jira, Confluence).""" import base64 import hashlib import os import re import time from abc import ABC, abstractmethod import requests from CommonServerPython import * # noqa: F401 ATLASSIAN_AUTH_URL = "https://auth.atlassian.com" class AtlassianOAuthClient(ABC): """ Abstract base class for Atlassian OAuth 2.0 authentication. This class provides common OAuth 2.0 functionality for Atlassian Cloud products. Child classes should implement the abstract methods for their specific use cases. """ def __init__( self, client_id: str, client_secret: str, callback_url: str, cloud_id: str = "", verify: bool = True, proxy: bool = False, ): """ Initialize the Atlassian OAuth client. Args: client_id: OAuth 2.0 Client ID client_secret: OAuth 2.0 Client Secret callback_url: OAuth callback/redirect URL cloud_id: Jira Cloud ID (required for Cloud instances) verify: Whether to verify SSL certificates proxy: Whether to use proxy settings """ self.client_id = client_id self.client_secret = client_secret self.callback_url = callback_url self.cloud_id = cloud_id self.verify = verify self.proxy = proxy @abstractmethod def get_oauth_scopes(self) -> list[str]: """ Return the list of OAuth scopes required for this integration. Returns: List of OAuth scope strings """ def get_access_token(self) -> str: """ Get the access token from integration context or refresh if expired. Returns: Valid access token Raises: DemistoException: If no access token or refresh token is available """ integration_context = get_integration_context() token = integration_context.get("token", "") if not token: raise DemistoException( "No access token configured. Please complete the authorization process using " "the oauth-start and oauth-complete commands" ) # Check if token is expired (with 10 second buffer) valid_until = integration_context.get("valid_until", 0) current_time = time.time() if current_time >= valid_until - 10: refresh_token = integration_context.get("refresh_token", "") if not refresh_token: raise DemistoException("No refresh token configured. Please complete the authorization process") # Refresh the access token self.oauth2_retrieve_access_token(refresh_token=refresh_token) integration_context = get_integration_context() token = integration_context.get("token", "") return token def oauth2_retrieve_access_token(self, code: str = "", refresh_token: str = "") -> None: """ Exchange authorization code or refresh token for access token. Args: code: Authorization code from OAuth callback refresh_token: Refresh token for renewing access token Raises: DemistoException: If both or neither code and refresh_token are provided """ if code and refresh_token: raise DemistoException("Both authorization code and refresh token were provided. Please provide only one") if not (code or refresh_token): raise DemistoException("No authorization code or refresh token provided") grant_type = "authorization_code" if code else "refresh_token" data = assign_params( client_id=self.client_id, client_secret=self.client_secret, code=code, redirect_uri=self.callback_url if code else "", refresh_token=refresh_token, grant_type=grant_type, ) token_url = urljoin(ATLASSIAN_AUTH_URL, "oauth/token") demisto.debug(f"Requesting OAuth token from {token_url} with grant_type={grant_type}") try: response = requests.post( token_url, data=data, verify=self.verify, proxies=handle_proxy() if self.proxy else None, timeout=60, ) response.raise_for_status() except requests.exceptions.HTTPError as e: demisto.debug(f"OAuth token request failed with status {e.response.status_code}: {e.response.text}") raise DemistoException(f"Failed to retrieve OAuth token (HTTP {e.response.status_code}): {e.response.text}") from e except requests.exceptions.RequestException as e: demisto.debug(f"OAuth token request failed: {e}") raise DemistoException(f"Failed to connect to OAuth token endpoint: {e}") from e res_access_token = response.json() demisto.debug("Successfully retrieved OAuth token") integration_context = get_integration_context() new_authorization_context = { "token": res_access_token.get("access_token", ""), "scopes": res_access_token.get("scope", ""), "valid_until": time.time() + res_access_token.get("expires_in", 0), "refresh_token": res_access_token.get("refresh_token", ""), } integration_context |= new_authorization_context set_integration_context(integration_context) def oauth_start(self) -> str: """ Start OAuth flow and return authorization URL. Constructs the Atlassian OAuth 2.0 (3LO) authorization URL with all required parameters including the 'state' parameter (required by Atlassian). Returns: Authorization URL for user to visit Raises: DemistoException: If no URL is returned """ scopes = self.get_oauth_scopes() # Generate a random state parameter for CSRF protection (required by Atlassian) state = base64.urlsafe_b64encode(os.urandom(24)).decode("utf-8") # Store state in integration context for validation on callback integration_context = get_integration_context() integration_context["oauth_state"] = state set_integration_context(integration_context) params = assign_params( audience="api.atlassian.com", client_id=self.client_id, scope=" ".join(scopes), redirect_uri=self.callback_url, state=state, response_type="code", prompt="consent", ) # Build the authorization URL directly instead of making a request # Atlassian OAuth 2.0 (3LO) requires the user to visit the URL directly from urllib.parse import urlencode auth_url = f"{ATLASSIAN_AUTH_URL}/authorize?{urlencode(params)}" return auth_url def oauth_complete(self, code: str) -> None: """ Complete OAuth flow by exchanging authorization code for tokens. Args: code: Authorization code from OAuth callback """ self.oauth2_retrieve_access_token(code=code) def test_connection(self) -> None: """ Test the OAuth connection by attempting to get an access token. Raises: DemistoException: If authentication fails """ self.get_access_token() class JiraCloudOAuthClient(AtlassianOAuthClient): """OAuth client specifically for Jira Cloud instances.""" def get_oauth_scopes(self) -> list[str]: """ Return the OAuth scopes required for Jira Cloud. The audit log API requires both the classic 'manage:jira-configuration' scope and the granular 'read:audit-log:jira' scope. The classic scope is needed because the audit endpoint is an admin-level API. Both must also be configured in the Atlassian Developer Console for the OAuth app. Returns: List of required OAuth scopes """ return [ "read:audit-log:jira", # Granular scope for audit log access "manage:jira-configuration", # Classic admin scope required by audit API "read:jira-work", # Required for API v3 access "read:jira-user", # User info access "offline_access", # For refresh token ] class ConfluenceCloudOAuthClient(AtlassianOAuthClient): """OAuth client specifically for Confluence Cloud instances.""" def get_oauth_scopes(self) -> list[str]: """ Return the OAuth scopes required for Confluence Cloud. Returns: List of required OAuth scopes """ return [ "read:audit-log:confluence", "read:confluence-content.all", "read:confluence-space.summary", "read:confluence-user", "read:confluence-groups", "write:confluence-content", "write:confluence-space", "offline_access", # For refresh token ] class JiraOnPremOAuthClient(AtlassianOAuthClient): """OAuth client specifically for Jira On-Prem/Data Center instances.""" def __init__( self, client_id: str, client_secret: str, callback_url: str, server_url: str, verify: bool = True, proxy: bool = False, ): """ Initialize the Jira On-Prem OAuth client. Args: client_id: OAuth 2.0 Client ID client_secret: OAuth 2.0 Client Secret callback_url: OAuth callback/redirect URL server_url: Jira server URL (e.g., https://jira.company.com) verify: Whether to verify SSL certificates proxy: Whether to use proxy settings """ super().__init__( client_id=client_id, client_secret=client_secret, callback_url=callback_url, cloud_id="", # Not used for on-prem verify=verify, proxy=proxy, ) self.server_url = server_url.rstrip("/") def get_oauth_scopes(self) -> list[str]: """ Return the OAuth scopes required for Jira On-Prem. Returns: List of required OAuth scopes (On-Prem uses single scope string) """ return ["ADMIN"] def oauth_start(self) -> str: """ Start OAuth flow for On-Prem and return authorization URL. Uses PKCE (Proof Key for Code Exchange) for enhanced security. Returns: Authorization URL for user to visit Raises: DemistoException: If no URL is returned """ integration_context = get_integration_context() try: # Generate PKCE code verifier and challenge code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier) code_challenge_bytes = hashlib.sha256(code_verifier.encode("utf-8")).digest() code_challenge = base64.urlsafe_b64encode(code_challenge_bytes).decode("utf-8") code_challenge = code_challenge.replace("=", "") # Store code_verifier in integration context for later use integration_context["code_verifier"] = code_verifier set_integration_context(integration_context) params = assign_params( client_id=self.client_id, scope=" ".join(self.get_oauth_scopes()), redirect_uri=self.callback_url, code_challenge=code_challenge, code_challenge_method="S256", response_type="code", ) authorize_url = f"{self.server_url}/rest/oauth2/latest/authorize" demisto.debug(f"Requesting On-Prem OAuth authorization from {authorize_url}") response = requests.get( authorize_url, params=params, verify=self.verify, proxies=handle_proxy() if self.proxy else None, allow_redirects=False, timeout=60, ) demisto.debug(f"On-Prem OAuth authorization response status: {response.status_code}") if response.url: return response.url raise DemistoException("No authorization URL was returned") except Exception: # Clean up code_verifier from integration context on failure integration_context.pop("code_verifier", None) set_integration_context(integration_context) raise def oauth2_retrieve_access_token(self, code: str = "", refresh_token: str = "") -> None: """ Exchange authorization code or refresh token for access token (On-Prem version). Args: code: Authorization code from OAuth callback refresh_token: Refresh token for renewing access token Raises: DemistoException: If both or neither code and refresh_token are provided """ if code and refresh_token: raise DemistoException("Both authorization code and refresh token were provided. Please provide only one") if not (code or refresh_token): raise DemistoException("No authorization code or refresh token provided") integration_context = get_integration_context() # Pop code_verifier as it's only needed during initial authorization code_verifier = integration_context.pop("code_verifier", "") grant_type = "authorization_code" if code else "refresh_token" data = assign_params( client_id=self.client_id, client_secret=self.client_secret, code_verifier=code_verifier if code else "", code=code, redirect_uri=self.callback_url if code else "", refresh_token=refresh_token, grant_type=grant_type, ) token_url = f"{self.server_url}/rest/oauth2/latest/token" demisto.debug(f"Requesting On-Prem OAuth token from {token_url} with grant_type={grant_type}") try: response = requests.post( token_url, data=data, verify=self.verify, proxies=handle_proxy() if self.proxy else None, timeout=60, ) response.raise_for_status() except requests.exceptions.HTTPError as e: demisto.debug(f"On-Prem OAuth token request failed with status {e.response.status_code}: {e.response.text}") raise DemistoException( f"Failed to retrieve On-Prem OAuth token (HTTP {e.response.status_code}): {e.response.text}" ) from e except requests.exceptions.RequestException as e: demisto.debug(f"On-Prem OAuth token request failed: {e}") raise DemistoException(f"Failed to connect to On-Prem OAuth token endpoint: {e}") from e res_access_token = response.json() demisto.debug("Successfully retrieved On-Prem OAuth token") new_authorization_context = { "token": res_access_token.get("access_token", ""), "scopes": res_access_token.get("scope", ""), "valid_until": time.time() + res_access_token.get("expires_in", 0), "refresh_token": res_access_token.get("refresh_token", ""), } integration_context |= new_authorization_context set_integration_context(integration_context) def create_atlassian_oauth_client( client_id: str, client_secret: str, callback_url: str, cloud_id: str = "", server_url: str = "", verify: bool = True, proxy: bool = False, product: str = "jira", ): """ Factory function to create an Atlassian OAuth client. Automatically determines whether to create a Cloud or On-Prem client based on the presence of cloud_id. Args: client_id: OAuth 2.0 Client ID client_secret: OAuth 2.0 Client Secret callback_url: OAuth callback/redirect URL cloud_id: Cloud ID (if provided, creates Cloud client) server_url: Server URL (for On-Prem instances) verify: Whether to verify SSL certificates proxy: Whether to use proxy settings product: Product type ('jira' or 'confluence') Returns: Configured Cloud or On-Prem OAuth client instance """ if cloud_id: # Cloud instance if product.lower() == "confluence": return ConfluenceCloudOAuthClient( client_id=client_id, client_secret=client_secret, callback_url=callback_url, cloud_id=cloud_id, verify=verify, proxy=proxy, ) else: return JiraCloudOAuthClient( client_id=client_id, client_secret=client_secret, callback_url=callback_url, cloud_id=cloud_id, verify=verify, proxy=proxy, ) else: # On-Prem instance (currently only Jira is supported) return JiraOnPremOAuthClient( client_id=client_id, client_secret=client_secret, callback_url=callback_url, server_url=server_url, verify=verify, proxy=proxy, ) # Backward-compatible alias for integrations that import by the old name create_jira_oauth_client = create_atlassian_oauth_client
README
AtlassianApiModule
This API module provides shared OAuth 2.0 authentication functionality for Atlassian products (Jira, Confluence), supporting both Cloud and On-Prem/Data Center instances.
Classes
AtlassianOAuthClient (Abstract Base Class)
Base class for Atlassian OAuth 2.0 authentication with the following features:
- Access token management with automatic refresh
- OAuth 2.0 authorization flow
- Integration context storage for tokens
Methods
get_access_token(): Retrieves valid access token, refreshing if neededoauth2_retrieve_access_token(code, refresh_token): Exchanges code/refresh token for access tokenoauth_start(): Initiates OAuth flow and returns authorization URLoauth_complete(code): Completes OAuth flow with authorization codetest_connection(): Tests OAuth authentication
JiraCloudOAuthClient
Concrete implementation for Jira Cloud with predefined scopes:
read:audit-log:jiraread:jira-useroffline_access
JiraOnPremOAuthClient
Concrete implementation for Jira On-Prem/Data Center with:
- PKCE (Proof Key for Code Exchange) support for enhanced security
- Scope:
ADMIN(provides admin-level access required for audit log retrieval) - Server-specific OAuth endpoints
Usage
Jira Cloud
from AtlassianApiModule import create_atlassian_oauth_client
# Create OAuth client for Cloud
oauth_client = create_atlassian_oauth_client(
client_id="your-client-id",
client_secret="your-client-secret",
callback_url="https://localhost/callback",
cloud_id="your-cloud-id", # Required for Cloud
verify=True,
proxy=False
)
# Start OAuth flow
auth_url = oauth_client.oauth_start()
# Complete OAuth flow (after user authorizes)
oauth_client.oauth_complete(code="authorization-code")
# Get access token for API calls
access_token = oauth_client.get_access_token()
Jira On-Prem/Data Center
from AtlassianApiModule import create_atlassian_oauth_client
# Create OAuth client for On-Prem
oauth_client = create_atlassian_oauth_client(
client_id="your-client-id",
client_secret="your-client-secret",
callback_url="https://localhost/callback",
cloud_id="", # Empty for On-Prem
server_url="https://jira.company.com", # Your Jira server URL
verify=True,
proxy=False
)
# Start OAuth flow (uses PKCE)
auth_url = oauth_client.oauth_start()
# Complete OAuth flow (after user authorizes)
oauth_client.oauth_complete(code="authorization-code")
# Get access token for API calls
access_token = oauth_client.get_access_token()
Integration with Atlassian Integrations
This module is designed to be imported by Atlassian product integrations that need OAuth 2.0 support:
- Jira: JiraEventCollector (supports both Cloud and On-Prem), JiraV3 (future refactoring)
- Confluence: Future integrations
The module handles all OAuth token lifecycle management, allowing integrations to focus on their core functionality.
Function Names
create_atlassian_oauth_client()- Generic factory function (recommended)create_jira_oauth_client()- Backward-compatible alias (same ascreate_atlassian_oauth_client)
Key Features
- Automatic Instance Detection: Factory function automatically creates the correct client type based on
cloud_idparameter - PKCE Support: On-Prem implementation uses PKCE for enhanced security
- Token Management: Automatic token refresh with 10-second expiry buffer
- Error Handling: Comprehensive error messages for troubleshooting