import requests import os import time from datetime import datetime, timedelta class OAuth2Client: def __init__(self, token_url, client_id, client_secret, scope=None): self.token_url = token_url self.client_id = client_id self.client_secret = client_secret self.scope = scope self.access_token = None self.expires_at = None # datetime object when token expires def _get_new_token(self): print("Requesting new OAuth2 access token...") headers = { "Content-Type": "application/x-www-form-urlencoded" } data = { "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret } if self.scope: data["scope"] = self.scope try: response = requests.post(self.token_url, headers=headers, data=data) response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) token_data = response.json() self.access_token = token_data.get("access_token") expires_in = token_data.get("expires_in") # seconds until expiration if self.access_token and expires_in: # Set expiration time a bit before actual expiration for safety (e.g., 60 seconds) self.expires_at = datetime.now() + timedelta(seconds=expires_in - 60) print(f"Successfully obtained new token. Expires at: {self.expires_at}") else: raise ValueError("Access token or expiration time missing from response.") except requests.exceptions.RequestException as e: print(f"Error obtaining OAuth2 token: {e}") self.access_token = None self.expires_at = None raise def get_access_token(self): # Check if token is still valid or needs refreshing if not self.access_token or (self.expires_at and datetime.now() >= self.expires_at): self._get_new_token() return self.access_token def make_api_request(self, api_url, method="GET", **kwargs): token = self.get_access_token() if not token: raise Exception("Could not obtain access token.") headers = kwargs.pop("headers", {}) headers["Authorization"] = f"Bearer {token}" try: response = requests.request(method, api_url, headers=headers, **kwargs) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if e.response.status_code == 401: # Token might be invalid, try to refresh and retry print("API request failed with 401. Attempting to refresh token and retry...") self.access_token = None # Invalidate current token token = self.get_access_token() # Get new token if not token: raise Exception("Could not refresh token after 401.") headers["Authorization"] = f"Bearer {token}" response = requests.request(method, api_url, headers=headers, **kwargs) # Retry request response.raise_for_status() return response.json() raise # Re-raise other HTTP errors except requests.exceptions.RequestException as e: print(f"API request failed: {e}") raise # --- Usage Example --- if __name__ == "__main__": # --- Configuration (ideally from environment variables) --- TOKEN_URL = os.getenv("OAUTH_TOKEN_URL", "https://your-oauth-server.com/oauth/token") CLIENT_ID = os.getenv("OAUTH_CLIENT_ID", "your_client_id") CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET", "your_client_secret") API_BASE_URL = os.getenv("API_BASE_URL", "https://your-resource-server.com/api/v1") API_SCOPE = "read write" # Optional scope if "your_client_id" in CLIENT_ID or "your-oauth-server" in TOKEN_URL: print("WARNING: Please configure OAUTH_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and API_BASE_URL environment variables or update values in script.") # Exit or use dummy values for demonstration if not configured TOKEN_URL = "https://example.com/oauth/token" # Placeholder CLIENT_ID = "test_client" # Placeholder CLIENT_SECRET = "test_secret" # Placeholder API_BASE_URL = "https://example.com/api/v1" # Placeholder client = OAuth2Client(TOKEN_URL, CLIENT_ID, CLIENT_SECRET, scope=API_SCOPE) try: # First API call - token will be obtained print(" --- Making first API call ---") data = client.make_api_request(f"{API_BASE_URL}/data") print(f"API Response (first call): {data}") # Simulate token expiration (for testing, do not do this in production) # client.expires_at = datetime.now() - timedelta(seconds=10) # Second API call - if token is still valid, it will be reused print(" --- Making second API call (should reuse token) ---") data = client.make_api_request(f"{API_BASE_URL}/another-endpoint") print(f"API Response (second call): {data}") # Wait until just before theoretical token expiration to force refresh print(" --- Waiting to force token refresh ---") # In a real scenario, client.expires_at would be set correctly. # Here, we'll simulate waiting by making expires_at expire soon if client.expires_at: wait_time = (client.expires_at - datetime.now()).total_seconds() + 5 # Wait 5 seconds past expiration if wait_time > 0: print(f"Waiting for {int(wait_time)} seconds to force token refresh...") time.sleep(wait_time) else: print("Token already expired or about to expire, will refresh on next call.") else: print("Cannot determine token expiration, assuming it will be refreshed if needed.") time.sleep(2) # Give it some time if expires_at was not set # Third API call - token should be refreshed print(" --- Making third API call (should trigger token refresh) ---") data = client.make_api_request(f"{API_BASE_URL}/status") print(f"API Response (third call, after refresh): {data}") except Exception as e: print(f"An error occurred during API interaction: {e}")