From 3b9873c304e5b829b0456d2d2dd07da791a89cd0 Mon Sep 17 00:00:00 2001 From: Kajetan Narkiewicz <102035785+Kajetan-Narkiewicz-Planview@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:28:01 +0100 Subject: [PATCH] Add new pages to the examples covering the Auth procedures - Add OAuth2 Authorization Code Flow example (py-oauth2-authorization-code/) - Complete flow with local callback server for OAuth redirect - Token refresh demonstration - Production-ready patterns with error handling - Add OAuth2 Client Credentials Flow example (py-oauth2-client-credentials/) - Robot/service account authentication - Reusable OAuth2ClientCredentials class with automatic token management - Multiple authentication method examples (Basic Auth, body params) - Add OAuth1 example (py-oauth1/) - Legacy authentication support - Complete 3-legged OAuth1 flow - Migration guidance to OAuth2 - Add AUTH_README.md with: - Decision guide for choosing authentication method - Quick comparison table - Security best practices - Troubleshooting guide - Update main readme.md with authentication section and example index Each example includes Python script, README with usage instructions, and requirements.txt for dependencies. https://planview.projectplace.com/#direct/card/26044157 --- examples/.gitignore | 24 + examples/AUTH_README.md | 252 +++++++++++ examples/py-oauth1/oauth1_flow.py | 285 ++++++++++++ examples/py-oauth1/readme.md | 95 ++++ examples/py-oauth1/requirements.txt | 3 + .../oauth2_authorization_code.py | 304 +++++++++++++ .../py-oauth2-authorization-code/readme.md | 218 +++++++++ .../requirements.txt | 1 + .../oauth2_client_credentials.py | 360 +++++++++++++++ .../py-oauth2-client-credentials/readme.md | 422 ++++++++++++++++++ .../requirements.txt | 1 + examples/readme.md | 47 +- 12 files changed, 2006 insertions(+), 6 deletions(-) create mode 100644 examples/.gitignore create mode 100644 examples/AUTH_README.md create mode 100644 examples/py-oauth1/oauth1_flow.py create mode 100644 examples/py-oauth1/readme.md create mode 100644 examples/py-oauth1/requirements.txt create mode 100644 examples/py-oauth2-authorization-code/oauth2_authorization_code.py create mode 100644 examples/py-oauth2-authorization-code/readme.md create mode 100644 examples/py-oauth2-authorization-code/requirements.txt create mode 100644 examples/py-oauth2-client-credentials/oauth2_client_credentials.py create mode 100644 examples/py-oauth2-client-credentials/readme.md create mode 100644 examples/py-oauth2-client-credentials/requirements.txt diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..b8be124 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual environments +.venv/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Local config files with credentials +.env +*.local.py diff --git a/examples/AUTH_README.md b/examples/AUTH_README.md new file mode 100644 index 0000000..b43a47f --- /dev/null +++ b/examples/AUTH_README.md @@ -0,0 +1,252 @@ +# Authentication Mechanisms for Planview ProjectPlace API + +This directory contains examples for all supported authentication methods for the Planview ProjectPlace API. + +## Available Authentication Methods + +### 1. OAuth2 Authorization Code Flow ⭐ Recommended for User Access +**Directory**: `py-oauth2-authorization-code/` + +Use this when you need to access resources **on behalf of a user**. + +**Best for:** +- Web applications +- Mobile apps +- Desktop applications +- Any integration where users authorize your app to access their data + +**Key Features:** +- User authorizes your application +- Access tokens valid for 30 days +- Refresh tokens valid for 120 days +- Can maintain indefinite access with refresh tokens + +[View Example →](./py-oauth2-authorization-code) + +--- + +### 2. OAuth2 Client Credentials Flow ⭐ Recommended for Service Accounts +**Directory**: `py-oauth2-client-credentials/` + +Use this for **robot/service account** authentication without user interaction. + +**Best for:** +- Server-to-server integrations +- Automated scripts and workflows +- Bulk data operations +- Account-wide integrations +- Background jobs and scheduled tasks + +**Key Features:** +- No user interaction required +- Account-wide access +- Simple authentication flow +- Access tokens valid for 30 days + +[View Example →](./py-oauth2-client-credentials) + +--- + +### 3. OAuth1 (Legacy) +**Directory**: `py-oauth1/` + +**⚠️ Legacy Method** - Only use for maintaining existing integrations. + +For new projects, use OAuth2 instead. + +[View Example →](./py-oauth1) + +--- + +## Quick Comparison + +| Method | Use Case | User Interaction | Token Lifetime | Refresh Token | +|--------|----------|------------------|----------------|---------------| +| **OAuth2 Authorization Code** | User access | Required | 30 days | Yes (120 days) | +| **OAuth2 Client Credentials** | Service accounts | Not required | 30 days | No (just request new) | +| **OAuth1** | Legacy | Required | Permanent | No | + +--- + +## Choosing the Right Authentication Method + +### Use OAuth2 Authorization Code Flow when: +✅ Your app needs to act on behalf of users +✅ You need user-specific permissions +✅ Building a web/mobile/desktop app +✅ Users should control access to their data + +### Use OAuth2 Client Credentials Flow when: +✅ Building server-to-server integration +✅ Need account-wide access (not user-specific) +✅ Automating bulk operations +✅ Running scheduled jobs +✅ No user interaction is possible/desired + +### Use OAuth1 only when: +⚠️ Maintaining existing OAuth1 integration +⚠️ Required by legacy systems + +--- + +## Getting Started + +### For User Authentication (OAuth2 Authorization Code) + +1. **Register your application** in ProjectPlace + - Go to Settings → Developer → Applications + - Create new application + - Note Client ID and Client Secret + - Set Redirect URI + +2. **Try the example** + ```bash + cd py-oauth2-authorization-code + pip install -r requirements.txt + # Edit oauth2_authorization_code.py with your credentials + python oauth2_authorization_code.py + ``` + +3. **Read the documentation** + - [OAuth2 Authorization Code README](./py-oauth2-authorization-code/readme.md) + - [API Documentation](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) + +### For Service Account Authentication (OAuth2 Client Credentials) + +1. **Get robot credentials** from your administrator + - Admin goes to Account Administration → Integration settings + - Create robot user + - Generate OAuth2 credentials + - Receive Client ID and Client Secret + +2. **Try the example** + ```bash + cd py-oauth2-client-credentials + pip install -r requirements.txt + # Edit oauth2_client_credentials.py with your credentials + python oauth2_client_credentials.py + ``` + +3. **Read the documentation** + - [OAuth2 Client Credentials README](./py-oauth2-client-credentials/readme.md) + - [Setup Guide](https://success.planview.com/Planview_ProjectPlace/Integrations/Integrate_with_Planview_Hub%2F%2FViz_(Beta)) + +--- + +## API Endpoints + +All authentication methods work with the same API endpoints: + +**Base URL**: `https://api.projectplace.com` + +**Common Endpoints:** +- `/1/user/me` - Get current user info +- `/1/user/me/projects` - Get workspaces +- `/1/projects/{id}/boards` - Get boards +- `/1/boards/{id}/columns` - Get board columns +- `/1/columns/{id}/cards` - Get cards +- `/2/account/projects` - Get all account workspaces (requires appropriate permissions) + +**Authorization Header:** +``` +Authorization: Bearer {access_token} +``` + +--- + +## Security Best Practices + +### Never Commit Credentials +❌ Don't commit `CLIENT_ID`, `CLIENT_SECRET`, or tokens to version control + +✅ Use environment variables: +```python +import os +CLIENT_ID = os.environ.get('PROJECTPLACE_CLIENT_ID') +CLIENT_SECRET = os.environ.get('PROJECTPLACE_CLIENT_SECRET') +``` + +### Always Use HTTPS +❌ Never use HTTP endpoints +✅ Always use HTTPS: `https://api.projectplace.com` + +### Store Tokens Securely +❌ Don't store tokens in plain text +✅ Use encryption, secure vaults (AWS Secrets Manager, Azure Key Vault, etc.) + +### Rotate Credentials Regularly +✅ Generate new credentials periodically +✅ Revoke old credentials after rotation + +### Monitor API Usage +✅ Log all API calls for audit +✅ Set up alerts for unusual activity +✅ Implement rate limiting in your application + +--- + +## Additional Examples Using These Auth Methods + +Once you understand authentication, check out these examples that use the auth methods: + +- **py-download-document** - Download files using OAuth1 +- **py-upload-document** - Upload files +- **py-enforce-column-name** - Bulk board updates using Client Credentials +- **py-consume-odata** - Access OData feeds using Client Credentials +- **py-board-webhooks** - Set up webhooks +- **py-bulk-update-emails** - Bulk user operations + +--- + +## Troubleshooting + +### Common Issues + +**"Invalid client" error** +- Check your CLIENT_ID and CLIENT_SECRET +- Ensure no extra spaces or characters +- Verify credentials are for the correct environment + +**"Redirect URI mismatch" (OAuth2 Authorization Code)** +- Redirect URI in code must exactly match app settings +- Include protocol (http:// or https://) +- Match port number exactly + +**"Insufficient permissions"** +- For user auth: User must have appropriate access +- For robot auth: Robot account must be granted access by admin +- Check workspace/board-level permissions + +**Tokens expire immediately** +- Check your system clock is synchronized +- Token expiration is based on timestamps + +--- + +## API Documentation & Resources + +### Official Documentation +- [API Documentation](https://api.projectplace.com/apidocs) +- [OAuth2 Guide](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) +- [Success Center](https://success.planview.com/Planview_ProjectPlace) + +### Code Examples Repository +- [GitHub: api-code-examples](https://github.com/Projectplace/api-code-examples) + +### Support +- Contact your Planview administrator for robot account setup +- [Planview Support](https://success.planview.com) + +--- + +## Contributing + +Found an issue or have an improvement? Contributions welcome! + +--- + +**Last Updated**: March 2026 + +**Disclaimer**: Planview provides these examples for instructional purposes. While you are welcome to use this +code in any way you see fit, Planview does not accept any liability or responsibility for you choosing to do so. + diff --git a/examples/py-oauth1/oauth1_flow.py b/examples/py-oauth1/oauth1_flow.py new file mode 100644 index 0000000..8b17f57 --- /dev/null +++ b/examples/py-oauth1/oauth1_flow.py @@ -0,0 +1,285 @@ +""" +OAuth1 Authentication Example + +This example demonstrates how to use OAuth1 authentication with the +Planview ProjectPlace API. + +Note: OAuth1 is a legacy authentication method. For new integrations, +we recommend using OAuth2 (Authorization Code Flow or Client Credentials Flow). +""" + +import oauth2 +import requests +import webbrowser +import requests_oauthlib +from urllib.parse import parse_qs + +# Replace these with your application credentials +APPLICATION_KEY = 'REDACTED' +APPLICATION_SECRET = 'REDACTED' + +# OAuth1 endpoints +API_ENDPOINT = 'https://api.projectplace.com' +REQUEST_TOKEN_URL = f'{API_ENDPOINT}/initiate' +AUTHORIZE_URL = f'{API_ENDPOINT}/authorize' +ACCESS_TOKEN_URL = f'{API_ENDPOINT}/token' + + +def get_request_token(): + """ + Step 1: Obtain a request token + + Returns: + tuple: (oauth_token, oauth_token_secret) + """ + print('=== Step 1: Get Request Token ===') + + consumer = oauth2.Consumer(APPLICATION_KEY, APPLICATION_SECRET) + client = oauth2.Client(consumer) + + resp, content = client.request(REQUEST_TOKEN_URL, "GET") + + if resp['status'] != '200': + raise Exception(f"Failed to get request token: {content}") + + request_token = dict(parse_qs(content.decode('utf-8'))) + oauth_token = request_token['oauth_token'][0] + oauth_token_secret = request_token['oauth_token_secret'][0] + + print(f'✓ Request token obtained') + print(f' Token: {oauth_token[:20]}...') + print(f' Secret: {oauth_token_secret[:20]}...') + + return oauth_token, oauth_token_secret + + +def authorize_token(oauth_token): + """ + Step 2: Redirect user to authorize the token + + Args: + oauth_token (str): The request token + + Returns: + str: OAuth verifier code + """ + print('\n=== Step 2: Authorize Token ===') + + # Build authorization URL + auth_url = f'{AUTHORIZE_URL}?oauth_token={oauth_token}' + + print(f'Opening browser for authorization...') + print(f'URL: {auth_url}') + + # Open browser for user authorization + webbrowser.open(auth_url) + + print('\nAfter authorizing the application in your browser,') + print('you will see an OAuth verifier code.') + oauth_verifier = input('Enter the OAuth verifier: ') + + if not oauth_verifier: + raise Exception('OAuth verifier is required') + + print(f'✓ Verifier received: {oauth_verifier[:10]}...') + + return oauth_verifier + + +def get_access_token(oauth_token, oauth_token_secret, oauth_verifier): + """ + Step 3: Exchange request token for access token + + Args: + oauth_token (str): Request token + oauth_token_secret (str): Request token secret + oauth_verifier (str): Verifier code from authorization + + Returns: + tuple: (access_token, access_token_secret) + """ + print('\n=== Step 3: Get Access Token ===') + + consumer = oauth2.Consumer(APPLICATION_KEY, APPLICATION_SECRET) + token = oauth2.Token(oauth_token, oauth_token_secret) + token.set_verifier(oauth_verifier) + client = oauth2.Client(consumer, token) + + resp, content = client.request(ACCESS_TOKEN_URL, "GET") + + if resp['status'] != '200': + raise Exception(f"Failed to get access token: {content}") + + access_token_data = dict(parse_qs(content.decode('utf-8'))) + access_token = access_token_data['oauth_token'][0] + access_token_secret = access_token_data['oauth_token_secret'][0] + + print(f'✓ Access token obtained') + print(f' Token: {access_token[:20]}...') + print(f' Secret: {access_token_secret[:20]}...') + + return access_token, access_token_secret + + +def test_api_access(access_token, access_token_secret): + """ + Step 4: Make API calls using the access token + + Args: + access_token (str): OAuth1 access token + access_token_secret (str): OAuth1 access token secret + """ + print('\n=== Step 4: Test API Access ===') + + # Create OAuth1 session + oauth1 = requests_oauthlib.OAuth1( + client_key=APPLICATION_KEY, + client_secret=APPLICATION_SECRET, + resource_owner_key=access_token, + resource_owner_secret=access_token_secret + ) + + # Test 1: Get user information + print('\n--- Fetching User Information ---') + response = requests.get(f'{API_ENDPOINT}/1/user/me', auth=oauth1) + response.raise_for_status() + user_data = response.json() + + print(f'✓ User information retrieved') + print(f' Name: {user_data.get("first_name")} {user_data.get("last_name")}') + print(f' Email: {user_data.get("email")}') + print(f' User ID: {user_data.get("id")}') + + # Test 2: Get user's workspaces + print('\n--- Fetching Workspaces ---') + response = requests.get(f'{API_ENDPOINT}/1/user/me/projects', auth=oauth1) + response.raise_for_status() + workspaces = response.json() + + print(f'✓ Found {len(workspaces)} workspace(s)') + for ws in workspaces[:5]: # Show first 5 + print(f' - {ws["name"]} (ID: {ws["id"]})') + + return oauth1 + + +def example_additional_api_calls(oauth1): + """ + Additional examples of API calls using OAuth1 + + Args: + oauth1: OAuth1 session object + """ + print('\n=== Additional API Examples ===') + + # Get a specific workspace's boards + print('\n--- Example: Fetching Boards ---') + + # First, get workspaces to find one to work with + response = requests.get(f'{API_ENDPOINT}/1/user/me/projects', auth=oauth1) + workspaces = response.json() + + if workspaces: + workspace_id = workspaces[0]['id'] + workspace_name = workspaces[0]['name'] + + print(f'Getting boards for workspace: {workspace_name}') + response = requests.get( + f'{API_ENDPOINT}/1/projects/{workspace_id}/boards', + auth=oauth1 + ) + + if response.ok: + boards = response.json() + print(f'✓ Found {len(boards)} board(s)') + for board in boards[:3]: # Show first 3 + print(f' - {board["name"]} (ID: {board["id"]})') + else: + print(f'Could not fetch boards: {response.status_code}') + + +def example_using_requests_oauthlib(): + """ + Alternative example using requests-oauthlib library directly + This is useful when you already have access tokens + """ + print('\n=== Alternative: Using requests-oauthlib Directly ===') + print('If you already have access tokens, you can use them directly:') + print('') + print('```python') + print('import requests') + print('import requests_oauthlib') + print('') + print('oauth1 = requests_oauthlib.OAuth1(') + print(' client_key=APPLICATION_KEY,') + print(' client_secret=APPLICATION_SECRET,') + print(' resource_owner_key=ACCESS_TOKEN,') + print(' resource_owner_secret=ACCESS_TOKEN_SECRET') + print(')') + print('') + print('response = requests.get(') + print(' "https://api.projectplace.com/1/user/me",') + print(' auth=oauth1') + print(')') + print('```') + + +def main(): + """ + Main function demonstrating complete OAuth1 flow + """ + print('==============================================') + print('OAuth1 Authentication Example') + print('==============================================') + print('\nNote: OAuth1 is a legacy authentication method.') + print('For new integrations, consider using OAuth2.') + print('') + print('This example will:') + print('1. Obtain a request token') + print('2. Open browser for user authorization') + print('3. Exchange for an access token') + print('4. Make API calls with the access token') + print('==============================================\n') + + try: + # Step 1: Get request token + oauth_token, oauth_token_secret = get_request_token() + + # Step 2: Authorize token + oauth_verifier = authorize_token(oauth_token) + + # Step 3: Get access token + access_token, access_token_secret = get_access_token( + oauth_token, + oauth_token_secret, + oauth_verifier + ) + + # Step 4: Test API access + oauth1_session = test_api_access(access_token, access_token_secret) + + # Additional examples + example_additional_api_calls(oauth1_session) + + # Show how to use tokens directly + example_using_requests_oauthlib() + + print('\n==============================================') + print('✓ OAuth1 flow completed successfully!') + print('==============================================') + print('\nYour OAuth1 Credentials:') + print(f'Application Key: {APPLICATION_KEY}') + print(f'Application Secret: {APPLICATION_SECRET}') + print(f'Access Token: {access_token}') + print(f'Access Token Secret: {access_token_secret}') + print('\nStore these credentials securely to use in your application.') + print('OAuth1 tokens do not expire, but can be revoked by the user.') + + except Exception as e: + print(f'\n❌ Error: {e}') + import traceback + traceback.print_exc() + + +if __name__ == '__main__': + main() diff --git a/examples/py-oauth1/readme.md b/examples/py-oauth1/readme.md new file mode 100644 index 0000000..a6ba341 --- /dev/null +++ b/examples/py-oauth1/readme.md @@ -0,0 +1,95 @@ +**Disclaimer**: Planview provides these examples for instructional purposes. While you are welcome to use this +code in any way you see fit - Planview does not accept any liability or responsibility for you choosing to do so. + +# OAuth1 Authentication Example + +This example demonstrates how to use OAuth1 authentication with the Planview ProjectPlace API. + +## Important Note + +**OAuth1 is a legacy authentication method.** For new integrations, we strongly recommend using: +- **OAuth2 Authorization Code Flow** - For user authentication +- **OAuth2 Client Credentials Flow** - For robot/service accounts + +However, this example is provided for maintaining existing OAuth1 integrations. + +## Prerequisites + +### Install Requirements + +```bash +pip install -r requirements.txt +``` + +Required packages: +- `requests` - HTTP library +- `requests-oauthlib` - OAuth1 support for requests +- `oauth2` - OAuth1 protocol implementation + +### Configuration + +Edit the script and replace these values: + +```python +APPLICATION_KEY = 'your_application_key_here' +APPLICATION_SECRET = 'your_application_secret_here' +``` + +## Usage + +```bash +python oauth1_flow.py +``` + +The script will: +1. Request a temporary request token +2. Open your browser for authorization +3. Prompt you to enter the OAuth verifier code +4. Exchange for permanent access tokens +5. Demonstrate API calls + +## Making API Calls + +Once you have access tokens: + +```python +import requests +import requests_oauthlib + +oauth1 = requests_oauthlib.OAuth1( + client_key=APPLICATION_KEY, + client_secret=APPLICATION_SECRET, + resource_owner_key=ACCESS_TOKEN, + resource_owner_secret=ACCESS_TOKEN_SECRET +) + +response = requests.get( + 'https://api.projectplace.com/1/user/me', + auth=oauth1 +) + +user_data = response.json() +``` + +## Token Characteristics + +- **Request Token**: Temporary token used for authorization (expires quickly) +- **Access Token**: Permanent token for API access (does not expire but can be revoked) +- **No Refresh Token**: OAuth1 tokens don't expire, so no refresh mechanism is needed + +## Migration to OAuth2 + +For new projects, use OAuth2 instead: +- **[OAuth2 Authorization Code Flow](../py-oauth2-authorization-code/)** - For user authentication +- **[OAuth2 Client Credentials Flow](../py-oauth2-client-credentials/)** - For service accounts + +## Documentation + +For complete API documentation: +- [API Reference](https://api.projectplace.com/apidocs) +- [OAuth2 Guide](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) + +## Related Examples + +- **py-download-document** - Example using OAuth1 for document operations + diff --git a/examples/py-oauth1/requirements.txt b/examples/py-oauth1/requirements.txt new file mode 100644 index 0000000..63dc8bf --- /dev/null +++ b/examples/py-oauth1/requirements.txt @@ -0,0 +1,3 @@ +requests +requests-oauthlib +oauth2 diff --git a/examples/py-oauth2-authorization-code/oauth2_authorization_code.py b/examples/py-oauth2-authorization-code/oauth2_authorization_code.py new file mode 100644 index 0000000..8ee9c76 --- /dev/null +++ b/examples/py-oauth2-authorization-code/oauth2_authorization_code.py @@ -0,0 +1,304 @@ +""" +OAuth2 Authorization Code Flow Example + +This example demonstrates how to implement the OAuth2 Authorization Code Flow +for user authentication with the Planview ProjectPlace API. + +This flow is used when you need to access resources on behalf of a user. +""" + +import time +import requests +import threading +import webbrowser +from urllib.parse import urlencode, urlparse, parse_qs +from http.server import HTTPServer, BaseHTTPRequestHandler + +# Replace these with your application credentials +CLIENT_ID = 'REDACTED' +CLIENT_SECRET = 'REDACTED' +REDIRECT_URI = 'http://localhost:8080/callback' # Must match your app settings +API_ENDPOINT = 'https://api.projectplace.com' + +# Global variables to store the authorization result +authorization_code = None +authorization_error = None +auth_server = None + + +class CallbackHandler(BaseHTTPRequestHandler): + """HTTP handler to receive the OAuth callback""" + + def do_GET(self): + global authorization_code + + # Parse the URL path and query parameters + parsed_url = urlparse(self.path) + query = parse_qs(parsed_url.query) + + # Only process requests to the callback path + if parsed_url.path != '/callback': + # Ignore unrelated requests (e.g., /favicon.ico) + self.send_response(404) + self.end_headers() + return + + if 'code' in query: + authorization_code = query['code'][0] + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b""" + + +

Authorization Successful!

+

You can close this window and return to the terminal.

+ + + """) + # Shutdown the server after receiving a valid authorization code + threading.Thread(target=self.server.shutdown).start() + elif 'error' in query: + global authorization_error + error = query['error'][0] + error_description = query.get('error_description', [''])[0] + # Store the error for the main thread + authorization_error = { + 'error': error, + 'description': error_description + } + self.send_response(400) + self.send_header('Content-type', 'text/html') + self.end_headers() + error_msg = f"{error}: {error_description}" if error_description else error + self.wfile.write(f""" + + +

Authorization Failed

+

Error: {error_msg}

+ + + """.encode()) + # Shutdown the server after receiving an error + threading.Thread(target=self.server.shutdown).start() + else: + # Callback path but missing required parameters + self.send_response(400) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b""" + + +

Invalid Request

+

Missing authorization code or error parameter.

+ + + """) + + def log_message(self, format, *args): + # Suppress log messages + pass + + +def start_callback_server(): + """Start a local HTTP server to receive the OAuth callback""" + global auth_server + auth_server = HTTPServer(('localhost', 8080), CallbackHandler) + auth_server.serve_forever() + + +def get_authorization_code(): + """ + Step 1: Redirect user to authorization page + Opens a browser window for the user to authorize the application + """ + global authorization_code + + # Start the callback server in a background thread + server_thread = threading.Thread(target=start_callback_server) + server_thread.daemon = True + server_thread.start() + + # Build the authorization URL + auth_params = { + 'client_id': CLIENT_ID, + 'redirect_uri': REDIRECT_URI, + 'state': 'random_state_string' # Should be random for security + } + + auth_url = f'{API_ENDPOINT}/oauth2/authorize?{urlencode(auth_params)}' + + print('\n=== Step 1: Get Authorization Code ===') + print(f'Opening browser to authorize application...') + print(f'URL: {auth_url}') + + # Open the browser + webbrowser.open(auth_url) + + # Wait for the callback + print('Waiting for authorization...') + timeout = 120 # 2 minutes timeout + start_time = time.time() + + while authorization_code is None and authorization_error is None and (time.time() - start_time) < timeout: + time.sleep(0.5) + + # Check for error first + if authorization_error is not None: + error_msg = authorization_error['error'] + if authorization_error['description']: + error_msg += f": {authorization_error['description']}" + raise Exception(f'Authorization failed - {error_msg}') + + if authorization_code is None: + raise Exception('Authorization timed out') + + print(f'✓ Authorization code received') + return authorization_code + + +def exchange_code_for_tokens(code): + """ + Step 2: Exchange authorization code for access token and refresh token + """ + print('\n=== Step 2: Exchange Code for Tokens ===') + + token_data = { + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'code': code, + 'grant_type': 'authorization_code' + } + + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data=token_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + response.raise_for_status() + tokens = response.json() + + print(f'✓ Access token received') + print(f' - Token type: {tokens["token_type"]}') + print(f' - Expires in: {tokens["expires"]} seconds ({tokens["expires"] / 86400:.1f} days)') + print(f' - Access token: {tokens["access_token"][:20]}...') + print(f' - Refresh token: {tokens["refresh_token"][:20]}...') + + return tokens + + +def refresh_access_token(refresh_token): + """ + Step 3: Refresh access token using refresh token + This allows you to get a new access token without user interaction + """ + print('\n=== Step 3: Refresh Access Token ===') + + refresh_data = { + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'refresh_token': refresh_token, + 'grant_type': 'refresh_token' + } + + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data=refresh_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + response.raise_for_status() + tokens = response.json() + + print(f'✓ New access token received') + print(f' - New access token: {tokens["access_token"][:20]}...') + print(f' - New refresh token: {tokens["refresh_token"][:20]}...') + + return tokens + + +def test_api_access(access_token): + """ + Step 4: Use the access token to make API calls + Tests the access token by fetching user information + """ + print('\n=== Step 4: Test API Access ===') + + # Method 1: Using Authorization header (recommended) + headers = { + 'Authorization': f'Bearer {access_token}' + } + + response = requests.get( + f'{API_ENDPOINT}/1/user/me', + headers=headers + ) + + response.raise_for_status() + user_data = response.json() + + print(f'✓ API access successful!') + print(f' - User: {user_data.get("first_name")} {user_data.get("last_name")}') + print(f' - Email: {user_data.get("email")}') + print(f' - User ID: {user_data.get("id")}') + + # Method 2: Using query parameter (alternative) + # response = requests.get(f'{API_ENDPOINT}/1/user/me?access_token={access_token}') + + return user_data + + +def main(): + """ + Main function demonstrating the complete OAuth2 Authorization Code Flow + """ + print('==============================================') + print('OAuth2 Authorization Code Flow Example') + print('==============================================') + print('\nThis example will:') + print('1. Open a browser for you to authorize the app') + print('2. Exchange the authorization code for tokens') + print('3. Demonstrate refreshing the access token') + print('4. Make a test API call') + print('\nNote: Make sure your REDIRECT_URI matches your') + print(' application settings in ProjectPlace') + print('==============================================\n') + + try: + # Step 1: Get authorization code + code = get_authorization_code() + + # Step 2: Exchange code for tokens + tokens = exchange_code_for_tokens(code) + access_token = tokens['access_token'] + refresh_token = tokens['refresh_token'] + + # Step 3: Test API access + test_api_access(access_token) + + # Step 4: Demonstrate token refresh + print('\n--- Demonstrating Token Refresh ---') + new_tokens = refresh_access_token(refresh_token) + + # Test with new token + test_api_access(new_tokens['access_token']) + + print('\n==============================================') + print('✓ OAuth2 flow completed successfully!') + print('==============================================') + print('\nToken Information:') + print(f'Access Token: {new_tokens["access_token"]}') + print(f'Refresh Token: {new_tokens["refresh_token"]}') + print(f'Expires in: {new_tokens["expires"]} seconds') + print('\nStore these tokens securely to use in your application.') + print('Use the refresh token to get new access tokens when needed.') + + except Exception as e: + print(f'\n❌ Error: {e}') + import traceback + traceback.print_exc() + + +if __name__ == '__main__': + main() diff --git a/examples/py-oauth2-authorization-code/readme.md b/examples/py-oauth2-authorization-code/readme.md new file mode 100644 index 0000000..1644f9d --- /dev/null +++ b/examples/py-oauth2-authorization-code/readme.md @@ -0,0 +1,218 @@ +**Disclaimer**: Planview provides these examples for instructional purposes. While you are welcome to use this +code in any way you see fit - Planview does not accept any liability or responsibility for you choosing to do so. + +# OAuth2 Authorization Code Flow Example + +This example demonstrates how to implement the OAuth2 Authorization Code Flow for authenticating users with the Planview ProjectPlace API. + +## When to Use This Flow + +Use the **Authorization Code Flow** when: +- Your application needs to access resources **on behalf of a user** +- You need the user to authorize your application +- You're building a web application, mobile app, or desktop application +- You want to maintain long-term access using refresh tokens + +## Overview + +The OAuth2 Authorization Code Flow consists of these steps: + +1. **Redirect user to authorization page** - User authorizes your application +2. **Receive authorization code** - User is redirected back with a code +3. **Exchange code for tokens** - Get access token and refresh token +4. **Use access token** - Make API calls on behalf of the user +5. **Refresh token** - Get new access tokens without user interaction + +## Token Lifetimes + +- **Access Token**: Valid for **30 days** +- **Refresh Token**: Valid for **120 days** + +As long as you refresh your access token within 120 days, you can maintain indefinite access without requiring the user to re-authorize. + +## Prerequisites + +### 1. Register Your Application + +Before you can use OAuth2, you must register your application in ProjectPlace: + +1. Log in to ProjectPlace +2. Go to **User Settings** → **Developer** → **Applications** +3. Create a new application +4. Note your **Client ID** (Application Key) and **Client Secret** +5. Set your **Redirect URI** (e.g., `http://localhost:8080/callback` for local testing) + +### 2. Install Requirements + +```bash +pip install -r requirements.txt +``` + +The required package is: +- `requests` - For making HTTP requests + +## Configuration + +Edit the script and replace these values: + +```python +CLIENT_ID = 'your_client_id_here' +CLIENT_SECRET = 'your_client_secret_here' +REDIRECT_URI = 'http://localhost:8080/callback' # Must match your app settings +``` + +**Important**: The `REDIRECT_URI` must exactly match the redirect URI configured in your application settings in ProjectPlace. + +## Usage + +### Running the Example + +```bash +python oauth2_authorization_code.py +``` + +### What Happens + +1. The script starts a local HTTP server on port 8080 +2. Your web browser opens to the ProjectPlace authorization page +3. You log in and authorize the application +4. ProjectPlace redirects back to the local server with an authorization code +5. The script exchanges the code for access and refresh tokens +6. The script demonstrates making API calls with the access token +7. The script demonstrates refreshing the access token + +### Example Output + +``` +============================================== +OAuth2 Authorization Code Flow Example +============================================== + +=== Step 1: Get Authorization Code === +Opening browser to authorize application... +Waiting for authorization... +✓ Authorization code received + +=== Step 2: Exchange Code for Tokens === +✓ Access token received + - Token type: Bearer + - Expires in: 2592000 seconds (30.0 days) + - Access token: a1b2c3d4e5f6g7h8i9j0... + - Refresh token: z9y8x7w6v5u4t3s2r1q0... + +=== Step 4: Test API Access === +✓ API access successful! + - User: John Doe + - Email: john.doe@example.com + - User ID: 12345 + +=== Step 3: Refresh Access Token === +✓ New access token received + - New access token: k1l2m3n4o5p6q7r8s9t0... + - New refresh token: p0o9i8u7y6t5r4e3w2q1... + +✓ OAuth2 flow completed successfully! +``` + +## Using the Tokens in Your Application + +### Making API Calls with Access Token + +Once you have an access token, you can make API calls in two ways: + +**Method 1: Authorization Header (Recommended)** + +```python +import requests + +headers = { + 'Authorization': f'Bearer {access_token}' +} + +response = requests.get( + 'https://api.projectplace.com/1/user/me', + headers=headers +) +``` + +**Method 2: Query Parameter** + +```python +response = requests.get( + f'https://api.projectplace.com/1/user/me?access_token={access_token}' +) +``` + +### Refreshing Access Tokens + +When your access token expires (after 30 days), use the refresh token to get a new one: + +```python +import requests + +refresh_data = { + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'refresh_token': refresh_token, + 'grant_type': 'refresh_token' +} + +response = requests.post( + 'https://api.projectplace.com/oauth2/access_token', + data=refresh_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'} +) + +tokens = response.json() +new_access_token = tokens['access_token'] +new_refresh_token = tokens['refresh_token'] +``` + +**Important**: Both the access token AND refresh token are replaced when you refresh. You must store the new refresh token. + +## Security Best Practices + +1. **Never commit credentials** - Don't commit your `CLIENT_ID` and `CLIENT_SECRET` to version control +2. **Use environment variables** - Store credentials in environment variables or secure configuration files +3. **Use HTTPS in production** - Always use HTTPS for your redirect URI in production +4. **Validate state parameter** - Use the `state` parameter to prevent CSRF attacks +5. **Store tokens securely** - Never store tokens in plain text; use secure storage mechanisms +6. **Use HTTPS redirect URIs** - In production, your redirect URI should use HTTPS + +## Troubleshooting + +### "Redirect URI mismatch" error + +Make sure the `REDIRECT_URI` in your code exactly matches the one configured in your application settings. + +### "Port already in use" error + +The callback server uses port 8080. If this port is in use, you can: +- Stop the process using port 8080 +- Modify the script to use a different port (update both the server and REDIRECT_URI) + +### Browser doesn't open + +If the browser doesn't open automatically, copy the URL from the console and paste it into your browser manually. + +### "Invalid client" error + +Check that your `CLIENT_ID` and `CLIENT_SECRET` are correct. + +## API Documentation + +For complete API documentation, visit: +- [OAuth2 Documentation](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) +- [API Reference](https://api.projectplace.com/apidocs) + +## Related Examples + +- **OAuth2 Client Credentials Flow** - For robot/service accounts +- **OAuth1 Flow** - Legacy authentication method + +## Support + +For more information about Planview ProjectPlace APIs: +- [Success Center](https://success.planview.com/Planview_ProjectPlace) +- [API Code Examples Repository](https://github.com/Projectplace/api-code-examples) + diff --git a/examples/py-oauth2-authorization-code/requirements.txt b/examples/py-oauth2-authorization-code/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/examples/py-oauth2-authorization-code/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/examples/py-oauth2-client-credentials/oauth2_client_credentials.py b/examples/py-oauth2-client-credentials/oauth2_client_credentials.py new file mode 100644 index 0000000..1896fab --- /dev/null +++ b/examples/py-oauth2-client-credentials/oauth2_client_credentials.py @@ -0,0 +1,360 @@ +""" +OAuth2 Client Credentials Flow Example + +This example demonstrates how to implement the OAuth2 Client Credentials Flow +for service account (robot) authentication with the Planview ProjectPlace API. + +This flow is used for application-to-application communication where no user +interaction is required. +""" + +import requests +import requests.auth +from datetime import datetime, timedelta + +# Replace these with your robot account credentials +CLIENT_ID = 'REDACTED' +CLIENT_SECRET = 'REDACTED' +API_ENDPOINT = 'https://api.projectplace.com' + + +class OAuth2ClientCredentials: + """ + A reusable class for managing OAuth2 Client Credentials authentication + """ + + def __init__(self, client_id, client_secret, api_endpoint=API_ENDPOINT): + self.client_id = client_id + self.client_secret = client_secret + self.api_endpoint = api_endpoint + self.access_token = None + self.token_expires_at = None + + def get_access_token(self, force_refresh=False): + """ + Get a valid access token, refreshing if necessary + + Args: + force_refresh (bool): Force getting a new token even if current one is valid + + Returns: + str: Valid access token + """ + # Check if we have a valid token + if not force_refresh and self.access_token and self.token_expires_at: + if datetime.now() < self.token_expires_at: + return self.access_token + + # Request a new token + print('Requesting new access token...') + + response = requests.post( + f'{self.api_endpoint}/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + }, + auth=requests.auth.HTTPBasicAuth(self.client_id, self.client_secret), + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + response.raise_for_status() + token_data = response.json() + + self.access_token = token_data['access_token'] + # Set expiration with a 5-minute buffer + expires_in = token_data.get('expires', 2592000) # Default 30 days + self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 300) + + print(f'✓ Access token obtained (expires in {expires_in} seconds)') + + return self.access_token + + def get_auth_headers(self): + """ + Get headers for authenticated API requests + + Returns: + dict: Headers with Authorization token + """ + token = self.get_access_token() + return { + 'Authorization': f'Bearer {token}' + } + + def get(self, endpoint, **kwargs): + """ + Make an authenticated GET request + + Args: + endpoint (str): API endpoint (without base URL) + **kwargs: Additional arguments to pass to requests.get + + Returns: + requests.Response: Response object + """ + headers = kwargs.get('headers') or {} + headers.update(self.get_auth_headers()) + kwargs['headers'] = headers + return requests.get(f'{self.api_endpoint}{endpoint}', **kwargs) + + def post(self, endpoint, **kwargs): + """ + Make an authenticated POST request + + Args: + endpoint (str): API endpoint (without base URL) + **kwargs: Additional arguments to pass to requests.post + + Returns: + requests.Response: Response object + """ + headers = kwargs.get('headers') or {} + headers.update(self.get_auth_headers()) + kwargs['headers'] = headers + return requests.post(f'{self.api_endpoint}{endpoint}', **kwargs) + + def put(self, endpoint, **kwargs): + """ + Make an authenticated PUT request + + Args: + endpoint (str): API endpoint (without base URL) + **kwargs: Additional arguments to pass to requests.put + + Returns: + requests.Response: Response object + """ + headers = kwargs.get('headers') or {} + headers.update(self.get_auth_headers()) + kwargs['headers'] = headers + return requests.put(f'{self.api_endpoint}{endpoint}', **kwargs) + + def delete(self, endpoint, **kwargs): + """ + Make an authenticated DELETE request + + Args: + endpoint (str): API endpoint (without base URL) + **kwargs: Additional arguments to pass to requests.delete + + Returns: + requests.Response: Response object + """ + headers = kwargs.get('headers') or {} + headers.update(self.get_auth_headers()) + kwargs['headers'] = headers + return requests.delete(f'{self.api_endpoint}{endpoint}', **kwargs) + + +def example_basic_usage(): + """ + Example 1: Basic usage of client credentials flow + """ + print('\n=== Example 1: Basic Token Request ===') + + # Method 1: Using Basic HTTP Authentication (recommended) + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + }, + auth=requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) + ) + + response.raise_for_status() + token_data = response.json() + + print(f'✓ Access token received') + print(f' Token type: {token_data["token_type"]}') + print(f' Access token: {token_data["access_token"][:20]}...') + print(f' Expires in: {token_data.get("expires", "N/A")} seconds') + + return token_data['access_token'] + + +def example_alternative_method(): + """ + Example 2: Alternative method - passing credentials in request body + """ + print('\n=== Example 2: Alternative Method (Body Parameters) ===') + + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + }, + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + response.raise_for_status() + token_data = response.json() + + print(f'✓ Access token received via alternative method') + + return token_data['access_token'] + + +def example_api_calls(access_token): + """ + Example 3: Making API calls with the access token + """ + print('\n=== Example 3: Making API Calls ===') + + headers = { + 'Authorization': f'Bearer {access_token}' + } + + # Get account information + print('\n--- Fetching Account Workspaces ---') + response = requests.post( + f'{API_ENDPOINT}/2/account/projects', + json={ + 'sort_by': '+creation_date', + 'filter': { + 'archive_status': [0] # Only active workspaces + }, + 'limit': 5 + }, + headers=headers + ) + + response.raise_for_status() + workspaces = response.json() + + print(f'✓ Found {len(workspaces)} workspace(s)') + for ws in workspaces: + print(f' - {ws["name"]} (ID: {ws["id"]})') + + # Get robot user information + print('\n--- Fetching Robot User Info ---') + response = requests.get( + f'{API_ENDPOINT}/1/user/me', + headers=headers + ) + + response.raise_for_status() + user_data = response.json() + + print(f'✓ Robot account details:') + print(f' - Name: {user_data.get("first_name")} {user_data.get("last_name")}') + print(f' - Email: {user_data.get("email")}') + print(f' - User ID: {user_data.get("id")}') + print(f' - Is Robot: {user_data.get("is_robot", False)}') + + +def example_reusable_client(): + """ + Example 4: Using the reusable OAuth2ClientCredentials class + """ + print('\n=== Example 4: Using Reusable Client Class ===') + + # Create a client instance + client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + + # The client automatically handles token management + print('\n--- First API Call ---') + response = client.get('/1/user/me') + response.raise_for_status() + user_data = response.json() + print(f'✓ User: {user_data.get("email")}') + + print('\n--- Second API Call (reuses token) ---') + response = client.post( + '/2/account/projects', + json={ + 'limit': 3, + 'filter': {'archive_status': [0]} + } + ) + response.raise_for_status() + workspaces = response.json() + print(f'✓ Found {len(workspaces)} workspaces') + + print('\n--- Force Token Refresh ---') + new_token = client.get_access_token(force_refresh=True) + print(f'✓ New token: {new_token[:20]}...') + + +def example_error_handling(): + """ + Example 5: Proper error handling + """ + print('\n=== Example 5: Error Handling ===') + + try: + # Intentionally use invalid credentials + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data={'grant_type': 'client_credentials'}, + auth=requests.auth.HTTPBasicAuth('invalid', 'invalid') + ) + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f'✓ Caught authentication error: {e.response.status_code}') + print(f' Error message: {e.response.text}') + + # Test with valid credentials + client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + + try: + # Try to access a resource that doesn't exist + response = client.get('/1/workspaces/999999999') + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f'✓ Caught API error: {e.response.status_code}') + if e.response.status_code == 404: + print(f' Resource not found or access denied') + + +def main(): + """ + Main function demonstrating OAuth2 Client Credentials Flow + """ + print('==============================================') + print('OAuth2 Client Credentials Flow Example') + print('==============================================') + print('\nThis flow is used for:') + print(' - Robot/service account authentication') + print(' - Application-to-application communication') + print(' - Account-wide operations') + print('\nNote: This requires a robot account set up') + print(' by your organization administrator') + print('==============================================\n') + + try: + # Example 1: Basic token request + access_token = example_basic_usage() + + # Example 2: Alternative method + example_alternative_method() + + # Example 3: Making API calls + example_api_calls(access_token) + + # Example 4: Using reusable client + example_reusable_client() + + # Example 5: Error handling + example_error_handling() + + print('\n==============================================') + print('✓ All examples completed successfully!') + print('==============================================') + + except requests.exceptions.HTTPError as e: + print(f'\n❌ HTTP Error: {e.response.status_code}') + print(f'Response: {e.response.text}') + print('\nCommon issues:') + print(' - Invalid CLIENT_ID or CLIENT_SECRET') + print(' - Robot account not properly configured') + print(' - Insufficient permissions') + except Exception as e: + print(f'\n❌ Error: {e}') + import traceback + traceback.print_exc() + + +if __name__ == '__main__': + main() diff --git a/examples/py-oauth2-client-credentials/readme.md b/examples/py-oauth2-client-credentials/readme.md new file mode 100644 index 0000000..f0801d0 --- /dev/null +++ b/examples/py-oauth2-client-credentials/readme.md @@ -0,0 +1,422 @@ +**Disclaimer**: Planview provides these examples for instructional purposes. While you are welcome to use this +code in any way you see fit - Planview does not accept any liability or responsibility for you choosing to do so. + +# OAuth2 Client Credentials Flow Example + +This example demonstrates how to implement the OAuth2 Client Credentials Flow for robot/service account authentication with the Planview ProjectPlace API. + +## When to Use This Flow + +Use the **Client Credentials Flow** when: +- Your application needs **account-wide access** (not on behalf of a specific user) +- You're using a **robot/service account** +- You're building server-to-server integrations +- No user interaction is required +- You need automated, programmatic access to your organization's data + +## Overview + +The Client Credentials Flow is simpler than the Authorization Code Flow: + +1. **Request access token** - Exchange client credentials for an access token +2. **Use access token** - Make API calls with the token +3. **Request new token when needed** - No refresh token is needed; just request a new access token + +## Key Differences from Authorization Code Flow + +| Feature | Client Credentials | Authorization Code | +|---------|-------------------|-------------------| +| User interaction | None required | User must authorize | +| Token lifetime | 30 days | 30 days (access) / 120 days (refresh) | +| Refresh token | Not provided | Provided | +| Use case | Service accounts | User accounts | +| Scope | Account-wide | User-specific | + +## Prerequisites + +### 1. Set Up Robot Account + +Before you can use this flow, your **organization administrator** must: + +1. Log in to ProjectPlace as an account administrator +2. Go to **Account administration** → **Integration settings** +3. Create a new robot user +4. Generate OAuth2 credentials for the robot +5. Provide you with the **Client ID** and **Client Secret** + +For detailed instructions, see: [How to Generate a Robot Token](https://success.planview.com/Planview_ProjectPlace/Integrations/Integrate_with_Planview_Hub%2F%2FViz_(Beta)) + +### 2. Install Requirements + +```bash +pip install -r requirements.txt +``` + +The required package is: +- `requests` - For making HTTP requests + +## Configuration + +Edit the script and replace these values: + +```python +CLIENT_ID = 'your_robot_client_id_here' +CLIENT_SECRET = 'your_robot_client_secret_here' +``` + +**Security Note**: Never commit these credentials to version control. Use environment variables or secure configuration management. + +## Usage + +### Running the Example + +```bash +python oauth2_client_credentials.py +``` + +The script demonstrates five examples: +1. Basic token request using HTTP Basic Authentication +2. Alternative method using body parameters +3. Making API calls with the access token +4. Using a reusable client class +5. Proper error handling + +### Example Output + +``` +============================================== +OAuth2 Client Credentials Flow Example +============================================== + +=== Example 1: Basic Token Request === +Requesting new access token... +✓ Access token obtained (expires in 2592000 seconds) +✓ Access token received + Token type: Bearer + Access token: a1b2c3d4e5f6g7h8i9j0... + Expires in: 2592000 seconds + +=== Example 3: Making API Calls === + +--- Fetching Account Workspaces --- +✓ Found 5 workspace(s) + - Project Alpha (ID: 12345) + - Marketing Campaign (ID: 12346) + - IT Infrastructure (ID: 12347) + +--- Fetching Robot User Info --- +✓ Robot account details: + - Name: API Robot + - Email: api.robot@example.com + - User ID: 98765 + - Is Robot: True + +✓ All examples completed successfully! +``` + +## Authentication Methods + +### Method 1: HTTP Basic Authentication (Recommended) + +```python +import requests +import requests.auth + +response = requests.post( + 'https://api.projectplace.com/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + }, + auth=requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) +) + +token = response.json()['access_token'] +``` + +### Method 2: Body Parameters + +```python +import requests + +response = requests.post( + 'https://api.projectplace.com/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + }, + headers={'Content-Type': 'application/x-www-form-urlencoded'} +) + +token = response.json()['access_token'] +``` + +## Using the Access Token + +### Making API Calls + +Once you have an access token, include it in the Authorization header: + +```python +import requests + +headers = { + 'Authorization': f'Bearer {access_token}' +} + +# Get workspaces +response = requests.post( + 'https://api.projectplace.com/2/account/projects', + json={ + 'sort_by': '+creation_date', + 'filter': { + 'archive_status': [0] # Active workspaces only + }, + 'limit': 10 + }, + headers=headers +) + +workspaces = response.json() +``` + +### Reusable Client Class + +The example includes a reusable `OAuth2ClientCredentials` class that: +- Automatically manages token lifecycle +- Handles token expiration +- Provides convenient methods for GET, POST, PUT, DELETE requests + +```python +from oauth2_client_credentials import OAuth2ClientCredentials + +# Create client +client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + +# Make requests (token is handled automatically) +response = client.get('/1/user/me') +user_data = response.json() + +response = client.post('/2/account/projects', json={'limit': 10}) +workspaces = response.json() +``` + +## Token Management + +### Token Expiration + +- Access tokens expire after **30 days** (2,592,000 seconds) +- No refresh token is provided +- To get a new token, simply repeat the client credentials flow + +### When to Request New Tokens + +The example client class automatically manages tokens, but if you're implementing your own logic: + +```python +from datetime import datetime, timedelta + +class TokenManager: + def __init__(self): + self.access_token = None + self.expires_at = None + + def is_token_valid(self): + if not self.access_token or not self.expires_at: + return False + # Add 5-minute buffer + return datetime.now() < (self.expires_at - timedelta(minutes=5)) + + def get_token(self): + if not self.is_token_valid(): + # Request new token + self.access_token = self.request_new_token() + self.expires_at = datetime.now() + timedelta(days=30) + return self.access_token +``` + +## Common Use Cases + +### 1. Bulk Data Export + +```python +client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + +# Get all workspaces +all_workspaces = [] +row_number = 0 +limit = 100 + +while True: + response = client.post('/2/account/projects', json={ + 'limit': limit, + 'row_number': row_number, + 'filter': {'archive_status': [0]} + }) + workspaces = response.json() + + if not workspaces: + break + + all_workspaces.extend(workspaces) + row_number += limit + +print(f'Total workspaces: {len(all_workspaces)}') +``` + +### 2. Automated Reporting + +```python +client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + +# Get all boards across workspaces +response = client.post('/2/account/projects', json={'limit': 1000}) +workspaces = response.json() + +for workspace in workspaces: + boards_response = client.get(f'/1/projects/{workspace["id"]}/boards') + boards = boards_response.json() + print(f'{workspace["name"]}: {len(boards)} boards') +``` + +### 3. Data Synchronization + +```python +client = OAuth2ClientCredentials(CLIENT_ID, CLIENT_SECRET) + +# Sync cards from a board +response = client.get('/1/boards/12345/columns') +columns = response.json() + +for column in columns: + cards_response = client.get(f'/1/columns/{column["id"]}/cards') + cards = cards_response.json() + # Process cards... +``` + +## Error Handling + +### Authentication Errors + +```python +try: + response = requests.post( + 'https://api.projectplace.com/oauth2/access_token', + data={'grant_type': 'client_credentials'}, + auth=requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) + ) + response.raise_for_status() +except requests.exceptions.HTTPError as e: + if e.response.status_code == 401: + print('Invalid credentials') + elif e.response.status_code == 403: + print('Robot account not authorized') +``` + +### API Errors + +```python +try: + response = client.get('/1/workspaces/999999') + response.raise_for_status() +except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print('Resource not found or access denied') + elif e.response.status_code == 429: + print('Rate limit exceeded') +``` + +## Security Best Practices + +1. **Never expose credentials** + - Don't commit `CLIENT_ID` and `CLIENT_SECRET` to version control + - Use environment variables or secure vaults (e.g., AWS Secrets Manager) + +2. **Rotate credentials regularly** + - Generate new credentials periodically + - Revoke old credentials after rotation + +3. **Limit robot permissions** + - Only grant necessary permissions to robot accounts + - Use different robots for different integration purposes + +4. **Monitor usage** + - Log all API calls for audit purposes + - Monitor for unusual activity patterns + +5. **Use HTTPS** + - Always use HTTPS endpoints + - Never send credentials over HTTP + +## Environment Variables (Recommended) + +Instead of hardcoding credentials, use environment variables: + +```python +import os + +CLIENT_ID = os.environ.get('PROJECTPLACE_CLIENT_ID') +CLIENT_SECRET = os.environ.get('PROJECTPLACE_CLIENT_SECRET') + +if not CLIENT_ID or not CLIENT_SECRET: + raise ValueError('Missing credentials in environment variables') +``` + +Set them in your environment: + +```bash +export PROJECTPLACE_CLIENT_ID='your_client_id' +export PROJECTPLACE_CLIENT_SECRET='your_client_secret' +python oauth2_client_credentials.py +``` + +## Troubleshooting + +### "Invalid client" error + +**Cause**: Incorrect `CLIENT_ID` or `CLIENT_SECRET` + +**Solution**: +- Verify credentials from your admin +- Ensure no extra spaces or characters +- Check if the robot account is still active + +### "Insufficient permissions" error + +**Cause**: Robot account lacks necessary permissions + +**Solution**: +- Contact your organization administrator +- Verify the robot has access to the resources you're trying to access +- Check if the workspace/board permissions are correctly configured + +### Token expires immediately + +**Cause**: System clock is incorrect + +**Solution**: +- Ensure your system clock is synchronized +- Token expiration is based on timestamps + +## API Documentation + +For complete API documentation: +- [OAuth2 Documentation](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) +- [Enterprise Integrations Guide](https://api.projectplace.com/apidocs#articles/pageEnterpriseIntegrations.html) +- [API Reference](https://api.projectplace.com/apidocs) +- [PUC API Documentation](https://github.com/Projectplace/api-code-examples) - For Universal Connector compatible endpoints + +## Related Examples + +- **OAuth2 Authorization Code Flow** - For user authentication +- **OAuth1 Flow** - Legacy authentication method +- **py-enforce-column-name** - Example using client credentials +- **py-consume-odata** - OData access with client credentials + +## Support + +For more information: +- [Success Center](https://success.planview.com/Planview_ProjectPlace) +- [API Code Examples Repository](https://github.com/Projectplace/api-code-examples) +- Contact your Planview administrator for robot account setup + diff --git a/examples/py-oauth2-client-credentials/requirements.txt b/examples/py-oauth2-client-credentials/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/examples/py-oauth2-client-credentials/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/examples/readme.md b/examples/readme.md index 764edfc..803cbc3 100644 --- a/examples/readme.md +++ b/examples/readme.md @@ -1,15 +1,50 @@ **Disclaimer**: Planview provides these examples for instructional purposes. While you are welcome to use this code in any way you see fit - Planview does not accept any liability or responsibility for you choosing to do so. -# API Code examples +# API Code Examples This directory contains examples of API usage across the Planview ProjectPlace product. -Some clarifications: +## Getting Started: Authentication -* Examples are written in a specific language - designated by the prefixes e.g `py-` for Python and `node-js` for -Node etc. +**NEW!** Before using any of these examples, start with our authentication guides: + +👉 **[Authentication Overview (AUTH_README.md)](./AUTH_README.md)** - Choose the right auth method for your needs + +### Quick Links to Authentication Examples: +- **[OAuth2 Authorization Code Flow](./py-oauth2-authorization-code/)** ⭐ Recommended for user access +- **[OAuth2 Client Credentials Flow](./py-oauth2-client-credentials/)** ⭐ Recommended for service accounts +- **[OAuth1 (Legacy)](./py-oauth1/)** - For maintaining existing integrations + +## Available Examples + +### Authentication Examples +- **py-oauth2-authorization-code** - OAuth2 user authentication flow +- **py-oauth2-client-credentials** - OAuth2 robot/service account authentication +- **py-oauth1** - Legacy OAuth1 authentication + +### Data Operations Examples +- **py-board-webhooks** - Set up and manage board webhooks +- **py-bulk-update-emails** - Bulk update user email addresses +- **py-consume-odata** - Access and download OData feeds +- **py-download-archived-workspaces** - Download archived workspace data +- **py-download-document** - Download documents from workspaces +- **py-enforce-column-name** - Bulk rename board columns across workspaces +- **py-list-document-archive** - List document archive contents +- **py-remove-inactive-users** - Remove inactive users from account +- **py-upload-document** - Upload documents to workspaces +- **node-js-import-cards-with-excel** - Import cards from Excel spreadsheets + +## About These Examples + +* Examples are written in a specific language - designated by the prefixes e.g `py-` for Python and `node-js` for Node etc. * The code herein is meant to be possible to run successfully with minor modifications for authentication. -* And as the disclaimer above states: while we encourage you to study the code to understand our APIs - we do - not accept responsibility for you running or modifying the code. +* All examples include README files with setup instructions and usage examples. +* As the disclaimer above states: while we encourage you to study the code to understand our APIs - we do not accept responsibility for you running or modifying the code. + +## Resources + +- [API Documentation](https://api.projectplace.com/apidocs) +- [OAuth2 Guide](https://api.projectplace.com/apidocs#articles/pageOAuth2.html) +- [Success Center](https://success.planview.com/Planview_ProjectPlace)