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 1/2] 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) From 1bdbffb7f796a4e9a438c9fa281cc92677807a63 Mon Sep 17 00:00:00 2001 From: Kajetan Narkiewicz <102035785+Kajetan-Narkiewicz-Planview@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:46:10 +0100 Subject: [PATCH 2/2] Remove old auth examples We shall maintain only Python examples to the auth procedures. Relevant docs are updated accordingly. --- .gitignore | 27 +- README.md | 51 ++- .../py-oauth1/oauth1_flow.py | 23 +- auth/py-oauth1/readme.md | 41 ++ {examples => auth}/py-oauth1/requirements.txt | 1 - .../oauth2_authorization_code.py | 94 ++++ auth/py-oauth2-authorization-code/readme.md | 27 ++ .../requirements.txt | 0 .../oauth2_client_credentials.py | 82 ++++ auth/py-oauth2-client-credentials/readme.md | 26 ++ .../requirements.txt | 0 examples/.gitignore | 24 - examples/AUTH_README.md | 252 ----------- examples/py-oauth1/oauth1_flow.py | 285 ------------ examples/py-oauth1/readme.md | 95 ---- .../oauth2_authorization_code.py | 304 ------------- .../py-oauth2-authorization-code/readme.md | 218 --------- .../oauth2_client_credentials.py | 360 --------------- .../py-oauth2-client-credentials/readme.md | 422 ------------------ examples/readme.md | 50 --- oauth1/csharp.cs | 71 --- oauth2/.npmrc | 2 - oauth2/csharp.cs | 184 -------- oauth2/node.js | 120 ----- oauth2/package.json | 16 - oauth2/python.py | 117 ----- oauth2/ruby.rb | 40 -- 27 files changed, 358 insertions(+), 2574 deletions(-) rename oauth1/python-robot.py => auth/py-oauth1/oauth1_flow.py (96%) create mode 100644 auth/py-oauth1/readme.md rename {examples => auth}/py-oauth1/requirements.txt (79%) create mode 100644 auth/py-oauth2-authorization-code/oauth2_authorization_code.py create mode 100644 auth/py-oauth2-authorization-code/readme.md rename {examples => auth}/py-oauth2-authorization-code/requirements.txt (100%) create mode 100644 auth/py-oauth2-client-credentials/oauth2_client_credentials.py create mode 100644 auth/py-oauth2-client-credentials/readme.md rename {examples => auth}/py-oauth2-client-credentials/requirements.txt (100%) delete mode 100644 examples/.gitignore delete mode 100644 examples/AUTH_README.md delete mode 100644 examples/py-oauth1/oauth1_flow.py delete mode 100644 examples/py-oauth1/readme.md delete mode 100644 examples/py-oauth2-authorization-code/oauth2_authorization_code.py delete mode 100644 examples/py-oauth2-authorization-code/readme.md delete mode 100644 examples/py-oauth2-client-credentials/oauth2_client_credentials.py delete mode 100644 examples/py-oauth2-client-credentials/readme.md delete mode 100644 examples/readme.md delete mode 100644 oauth1/csharp.cs delete mode 100644 oauth2/.npmrc delete mode 100644 oauth2/csharp.cs delete mode 100644 oauth2/node.js delete mode 100644 oauth2/package.json delete mode 100644 oauth2/python.py delete mode 100644 oauth2/ruby.rb diff --git a/.gitignore b/.gitignore index 759a7f6..f121466 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,32 @@ -/venv/ +# 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 + +# Token files access_token.json .access_token.json + +# Node.js node_modules/ package-lock.json diff --git a/README.md b/README.md index d67afc9..49069cf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,50 @@ -# api-code-examples +**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 + +This directory contains examples of API usage across the Planview ProjectPlace product. + +## Getting Started: Authentication + +**NEW!** Before using any of these examples, start with our authentication guides: + +👉 **[Authentication Overview (AUTH_README.md)](../auth/AUTH_README.md)** - Choose the right auth method for your needs + +### Quick Links to Authentication Examples: +- **[OAuth2 Authorization Code Flow](../auth/py-oauth2-authorization-code/)** ⭐ Recommended for user access +- **[OAuth2 Client Credentials Flow](../auth/py-oauth2-client-credentials/)** ⭐ Recommended for service accounts +- **[OAuth1 (Legacy)](../auth/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. +* 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) -Code examples for interacting with Planview ProjectPlace's public APIs diff --git a/oauth1/python-robot.py b/auth/py-oauth1/oauth1_flow.py similarity index 96% rename from oauth1/python-robot.py rename to auth/py-oauth1/oauth1_flow.py index 1eeaf31..b353948 100644 --- a/oauth1/python-robot.py +++ b/auth/py-oauth1/oauth1_flow.py @@ -1,4 +1,6 @@ """ +OAuth1 Authentication Example + Working example of how to use a "robot" account. A robot account is a hidden super user, which communicates over OAuth1 - as if that super @@ -15,19 +17,20 @@ For more information on OAuth1 see: https://service.projectplace.com/apidocs/#articles/pageOAuth1.html """ -import requests -import requests_oauthlib -import json -import os import sys +import json +import requests import textwrap import urllib.parse +import requests_oauthlib + APPLICATION_KEY = 'REDACTED' APPLICATION_SECRET = 'REDACTED' ACCESS_TOKEN_KEY = 'REDACTED' ACCESS_TOKEN_SECRET = 'REDACTED' -API_ENDPOINT = 'https://api.projectplace.com' +SUBDOMAIN = 'REDACTED' +API_ENDPOINT = f'https://{SUBDOMAIN}.projectplace.com' oauth = requests_oauthlib.OAuth1( client_key=APPLICATION_KEY, @@ -115,14 +118,14 @@ def print_account_info(): print(textwrap.dedent( """ Invoke the script such: - + `python python-robot.py account-info` - + or - + `python python-robot.py create-project PROJECT_NAME` - + (where project name is the intended name of the project) - + Since the script is for demo purposes - the created project is immediately deleted. You can comment out the delete invokation if you wish to keep it around.""")) diff --git a/auth/py-oauth1/readme.md b/auth/py-oauth1/readme.md new file mode 100644 index 0000000..da6cdb6 --- /dev/null +++ b/auth/py-oauth1/readme.md @@ -0,0 +1,41 @@ +**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 Robot Account Example + +Demonstrates how to use OAuth1 authentication with a robot account to make API calls. + +## Prerequisites + +```bash +pip install -r requirements.txt +``` + +## Configuration + +Edit the script and replace these values with your robot account credentials: + +```python +APPLICATION_KEY = 'your_application_key_here' +APPLICATION_SECRET = 'your_application_secret_here' +ACCESS_TOKEN_KEY = 'your_access_token_key_here' +ACCESS_TOKEN_SECRET = 'your_access_token_secret_here' +SUBDOMAIN = 'your_subdomain_here' +``` + +## Usage + +View account info: +```bash +python oauth1_flow.py account-info +``` + +Create a project (and immediately delete it): +```bash +python oauth1_flow.py create-project "My Project Name" +``` + +## Documentation + +- [API Reference](https://service.projectplace.com/apidocs/) +- [OAuth1 Guide](https://service.projectplace.com/apidocs/#articles/pageOAuth1.html) diff --git a/examples/py-oauth1/requirements.txt b/auth/py-oauth1/requirements.txt similarity index 79% rename from examples/py-oauth1/requirements.txt rename to auth/py-oauth1/requirements.txt index 63dc8bf..cbbb937 100644 --- a/examples/py-oauth1/requirements.txt +++ b/auth/py-oauth1/requirements.txt @@ -1,3 +1,2 @@ requests requests-oauthlib -oauth2 diff --git a/auth/py-oauth2-authorization-code/oauth2_authorization_code.py b/auth/py-oauth2-authorization-code/oauth2_authorization_code.py new file mode 100644 index 0000000..2c37107 --- /dev/null +++ b/auth/py-oauth2-authorization-code/oauth2_authorization_code.py @@ -0,0 +1,94 @@ +""" +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 random +import requests +import webbrowser +from urllib.parse import urlencode + +# Replace these with your application credentials +CLIENT_ID = 'REDACTED' +CLIENT_SECRET = 'REDACTED' +SUBDOMAIN = 'REDACTED' # e.g. 'mycompany' +REDIRECT_URI = 'https://oob' # Must match your app settings +API_ENDPOINT = f'https://{SUBDOMAIN}.projectplace.com' + + +def get_authorization_code(): + """ + Opens browser for user authorization and prompts for the auth code. + """ + auth_params = { + 'client_id': CLIENT_ID, + 'redirect_uri': REDIRECT_URI, + 'state': f'random_{random.randint(1000000, 10000000)}' + } + + auth_url = f'{API_ENDPOINT}/oauth2/authorize?{urlencode(auth_params)}' + + print(f'Opening browser for authorization...') + print(f'URL: {auth_url}') + webbrowser.open(auth_url) + + return input('Enter the authorization code: ') + + +def exchange_code_for_tokens(code): + """ + Exchange authorization code for access and refresh tokens. + """ + response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data={ + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'code': code, + 'grant_type': 'authorization_code' + } + ) + response.raise_for_status() + return response.json() + + +def fetch_user_profile(access_token): + """ + Fetch the current user's profile to verify the token works. + """ + response = requests.get( + f'{API_ENDPOINT}/1/user/me/profile', + headers={'Authorization': f'Bearer {access_token}'} + ) + response.raise_for_status() + return response.json() + + +def main(): + print('OAuth2 Authorization Code Flow Example') + print('=' * 40) + + # Step 1: Get authorization code + print('\nStep 1: Get authorization code') + code = get_authorization_code() + print(f'✓ Authorization code received') + + # Step 2: Exchange code for tokens + print('\nStep 2: Exchange code for tokens') + tokens = exchange_code_for_tokens(code) + print(f'✓ Access token received: {tokens["access_token"][:20]}...') + print(f' Expires in: {tokens["expires"]} seconds') + + # Step 3: Test API access + print('\nStep 3: Test API access') + user = fetch_user_profile(tokens['access_token']) + print(f'✓ Logged in as: {user.get("first_name")} {user.get("last_name")}') + print(f' Email: {user.get("email")}') + + +if __name__ == '__main__': + main() diff --git a/auth/py-oauth2-authorization-code/readme.md b/auth/py-oauth2-authorization-code/readme.md new file mode 100644 index 0000000..57e66fa --- /dev/null +++ b/auth/py-oauth2-authorization-code/readme.md @@ -0,0 +1,27 @@ +**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 +Demonstrates how to use OAuth2 Authorization Code flow for user authentication. +## Prerequisites +```bash +pip install -r requirements.txt +``` +## Configuration +Edit the script and replace these values with your application credentials: +```python +CLIENT_ID = 'your_client_id_here' +CLIENT_SECRET = 'your_client_secret_here' +SUBDOMAIN = 'your_subdomain_here' +``` +## Usage +```bash +python oauth2_authorization_code.py +``` +The script will: +1. Open your browser for authorization +2. Prompt you to enter the authorization code +3. Exchange the code for access tokens +4. Fetch your user profile to verify it works +## Documentation +- [API Reference](https://service.projectplace.com/apidocs/) +- [OAuth2 Guide](https://service.projectplace.com/apidocs/#articles/pageOAuth2.html) diff --git a/examples/py-oauth2-authorization-code/requirements.txt b/auth/py-oauth2-authorization-code/requirements.txt similarity index 100% rename from examples/py-oauth2-authorization-code/requirements.txt rename to auth/py-oauth2-authorization-code/requirements.txt diff --git a/auth/py-oauth2-client-credentials/oauth2_client_credentials.py b/auth/py-oauth2-client-credentials/oauth2_client_credentials.py new file mode 100644 index 0000000..c1bd8e8 --- /dev/null +++ b/auth/py-oauth2-client-credentials/oauth2_client_credentials.py @@ -0,0 +1,82 @@ +""" +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 + +# Replace these with your robot account credentials +CLIENT_ID = 'REDACTED' +CLIENT_SECRET = 'REDACTED' +SUBDOMAIN = 'REDACTED' # e.g. 'mycompany' +API_ENDPOINT = f'https://{SUBDOMAIN}.projectplace.com' + +access_token = None + + +def _ensure_access_token(): + """ + We ask for a new access token on every script run - in normal circumstances you can hold on to an access + token for longer than that. + + This function uses the client credentials flow which only works for robot accounts since it is intended for + application-to-application communication. So the client_id and client_secret needs to belong to a robot and the + resulting access token will also belong to the robot. + """ + global access_token + access_token_response = requests.post( + f'{API_ENDPOINT}/oauth2/access_token', + data={ + 'grant_type': 'client_credentials', + }, + auth=requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) + ) + access_token_response.raise_for_status() + access_token = access_token_response.json()['access_token'] + + +def fetch_projects(): + """ + Fetch the first 5 active workspaces from the account. + """ + response = requests.post( + f'{API_ENDPOINT}/2/account/projects', + json={ + 'sort_by': '+creation_date', + 'filter': { + 'archive_status': [0] # Only active workspaces + }, + 'limit': 5 + }, + headers={'Authorization': f'Bearer {access_token}'} + ) + response.raise_for_status() + return response.json() + + +def main(): + print('OAuth2 Client Credentials Flow Example') + print('=' * 40) + + # Get access token + print('\nRequesting access token...') + _ensure_access_token() + print(f'✓ Access token received: {access_token[:20]}...') + + # Fetch projects + print('\nFetching workspaces...') + workspaces = fetch_projects() + + print(f'✓ Found {len(workspaces)} workspace(s):') + for ws in workspaces: + print(f' - {ws["name"]} (ID: {ws["id"]})') + + +if __name__ == '__main__': + main() diff --git a/auth/py-oauth2-client-credentials/readme.md b/auth/py-oauth2-client-credentials/readme.md new file mode 100644 index 0000000..9196896 --- /dev/null +++ b/auth/py-oauth2-client-credentials/readme.md @@ -0,0 +1,26 @@ +**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 +Demonstrates how to use OAuth2 Client Credentials flow with a robot/service account. +## Prerequisites +```bash +pip install -r requirements.txt +``` +## Configuration +Edit the script and replace these values with your robot account credentials: +```python +CLIENT_ID = 'your_robot_client_id_here' +CLIENT_SECRET = 'your_robot_client_secret_here' +SUBDOMAIN = 'your_subdomain_here' +``` +## Usage +```bash +python oauth2_client_credentials.py +``` +The script will: +1. Request an access token using client credentials +2. Fetch the first 5 active workspaces from your account +## Documentation +- [API Reference](https://service.projectplace.com/apidocs/) +- [OAuth2 Guide](https://service.projectplace.com/apidocs/oauth2.html) +- [How to Generate a Robot Token](https://success.planview.com/Planview_ProjectPlace/Account_administration/017_Manage_Robots_in_the_Account) diff --git a/examples/py-oauth2-client-credentials/requirements.txt b/auth/py-oauth2-client-credentials/requirements.txt similarity index 100% rename from examples/py-oauth2-client-credentials/requirements.txt rename to auth/py-oauth2-client-credentials/requirements.txt diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index b8be124..0000000 --- a/examples/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index b43a47f..0000000 --- a/examples/AUTH_README.md +++ /dev/null @@ -1,252 +0,0 @@ -# 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 deleted file mode 100644 index 8b17f57..0000000 --- a/examples/py-oauth1/oauth1_flow.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -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 deleted file mode 100644 index a6ba341..0000000 --- a/examples/py-oauth1/readme.md +++ /dev/null @@ -1,95 +0,0 @@ -**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-oauth2-authorization-code/oauth2_authorization_code.py b/examples/py-oauth2-authorization-code/oauth2_authorization_code.py deleted file mode 100644 index 8ee9c76..0000000 --- a/examples/py-oauth2-authorization-code/oauth2_authorization_code.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -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 deleted file mode 100644 index 1644f9d..0000000 --- a/examples/py-oauth2-authorization-code/readme.md +++ /dev/null @@ -1,218 +0,0 @@ -**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-client-credentials/oauth2_client_credentials.py b/examples/py-oauth2-client-credentials/oauth2_client_credentials.py deleted file mode 100644 index 1896fab..0000000 --- a/examples/py-oauth2-client-credentials/oauth2_client_credentials.py +++ /dev/null @@ -1,360 +0,0 @@ -""" -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 deleted file mode 100644 index f0801d0..0000000 --- a/examples/py-oauth2-client-credentials/readme.md +++ /dev/null @@ -1,422 +0,0 @@ -**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/readme.md b/examples/readme.md deleted file mode 100644 index 803cbc3..0000000 --- a/examples/readme.md +++ /dev/null @@ -1,50 +0,0 @@ -**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 - -This directory contains examples of API usage across the Planview ProjectPlace product. - -## Getting Started: Authentication - -**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. -* 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) - diff --git a/oauth1/csharp.cs b/oauth1/csharp.cs deleted file mode 100644 index 5b7a396..0000000 --- a/oauth1/csharp.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using DevDefined.OAuth.Consumer; -using DevDefined.OAuth.Framework; -using Newtonsoft.Json; - -namespace oauth1robot -{ - public class Profile - { - public int id { get; set; } - public String first_name { get; set; } - public String last_name { get; set; } - } - - class MainClass - { - public static void Main(string[] args) - { - string requestTokenUrl = "https://api.projectplace.com/initiate"; - string authorizationUrl = "https://api.projectplace.com/authorize"; - string tokenUrl = "https://api.projectplace.com/token"; - string apiEndpoint = "https://api.projectplace.com"; - string consumerKey = "APPLICATION_KEY_GOES_HERE"; - string consumerSecret = "APPLICATION_SECRET_GOES_HERE"; - IToken accessToken = null; - - // 1. If you already have an access token - uncomment this section and enter it here. - //IToken accessToken = new TokenBase(); - //accessToken.Token = "ACCESS_TOKEN_KEY_GOES_HERE"; - //accessToken.TokenSecret = "ACCESS_TOKEN_SECRET_GOES_HERE"; - - // 2. Create the consumer context - OAuthConsumerContext consumerContext = new OAuthConsumerContext - { - ConsumerKey = consumerKey, - ConsumerSecret = consumerSecret, - SignatureMethod = SignatureMethod.HmacSha1, - UseHeaderForOAuthParameters = true, - }; - - // 3. Start session - OAuthSession session = new OAuthSession(consumerContext, requestTokenUrl, authorizationUrl, tokenUrl); - - // 4. If you do not have an access token, you will first have to authorize - // access. In this part we formulate a URI which you must open in a web-broser. - // Once you have completed the log-in and accepted access for the application - // You will be redirected to whatever page is in the applications callback - // Simply check the URL and look for the oauth_verifer parameter, and copy that - if (accessToken == null) { - IToken requestToken = session.GetRequestToken(); - - string authorizationLink = session.GetUserAuthorizationUrlForToken(requestToken); - - Console.WriteLine("Authorize this application by going to {0}", authorizationLink); - Console.WriteLine("Then enter the oauth_verifier here:"); - string verificationCode = Console.ReadLine(); - - accessToken = session.ExchangeRequestTokenForAccessToken(requestToken, verificationCode); - Console.WriteLine("Here is your new access token: {0} with secret: {1}\n", accessToken.Token, accessToken.TokenSecret); - } - - // 5. We have an access token - assign it to the session - session.AccessToken = accessToken; - - // 6. Lets ask for a protected resource, such as your own profile - string responseText = session.Request().Get().ForUrl(apiEndpoint + "/1/user/me/profile").ToString(); - Profile profile = JsonConvert.DeserializeObject(responseText); - Console.WriteLine("Successfully fetched profile for {0} {1}", profile.first_name, profile.last_name); - } - } -} diff --git a/oauth2/.npmrc b/oauth2/.npmrc deleted file mode 100644 index f6ffbc1..0000000 --- a/oauth2/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -# .npmrc -engine-strict=true diff --git a/oauth2/csharp.cs b/oauth2/csharp.cs deleted file mode 100644 index 94bceb0..0000000 --- a/oauth2/csharp.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using Newtonsoft.Json; -using System.Net.Http; -using System.Net; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.IO; - -namespace oauth2 -{ - public class Profile - { - public int id { get; set; } - public String first_name { get; set; } - public String last_name { get; set; } - public String email { get; set; } - }; - - public class AccessToken - { - public String token_type {get; set;} - public String access_token {get; set;} - public String expries {get; set;} - public String refresh_token {get; set;} - } - - - class MainClass - { - private static string redirectUri = "REDACTED"; // Check your app settings - private static string clientId = "REDACTED"; // Check your app settings - private static string clientSecret = "REDACTED"; // Check your app settings - private static HttpClient _httpClient = new HttpClient(); - private static string baseUri = "https://api.projectplace.com"; - private static string authUrl = baseUri + "/oauth2/authorize"; - private static string tokenUrl = baseUri + "/oauth2/access_token"; - private static Random rand = new Random(); - private static AccessToken accessToken; - - /* - Checks if an access token is stored to disk (in .atoken.json). If so, attempts to check if it is still - valid (by asking for the user profile). If it isn't valid: tenatively invokes "refreshAccessToken", to - see if the access token is still "refreshable". - */ - public static void ensureAccessToken() { - try { - using (StreamReader file = File.OpenText(@".atoken.json")) - { - JsonSerializer serializer = new JsonSerializer(); - accessToken = (AccessToken)serializer.Deserialize(file, typeof(AccessToken)); - } - - // No access token on disk - abort - } catch (FileNotFoundException) { - return; - } - - // Lets see if the access token is valid - if (accessToken.access_token is not null) { - Console.WriteLine("We have an access token, lets see if it is still valid, otherwise refresh it"); - - Profile profile = profileRequest().Result; - - if (profile is null) { - Console.WriteLine("Seems like the access token has expired - lets attempt refreshing it"); - - refreshAccessToken(); - } - } - } - - /* - Stores the access token in a local file (.atoken.json) - */ - public static void storeAccessToken() { - // Store access token locally - using (StreamWriter file = File.CreateText(@".atoken.json")) - { - JsonSerializer serializer = new JsonSerializer(); - serializer.Serialize(file, accessToken); - } - } - - /* - Attempts to refresh the access token - an access token is "refreshable" for two weeks. - */ - public static async void refreshAccessToken() - { - var values = new Dictionary - { - { "client_id", clientId }, - { "client_secret", clientSecret }, - { "refresh_token", accessToken.refresh_token }, - { "grant_type", "refresh_token" } - }; - - var requestBody = new FormUrlEncodedContent(values); - var response = await _httpClient.PostAsync(tokenUrl, requestBody); - if (response.IsSuccessStatusCode) { - var contents = await response.Content.ReadAsStringAsync(); - accessToken = JsonConvert.DeserializeObject(contents); - storeAccessToken(); - } - } - - - /* - Initial access token exchange - this should normally only happen once - provided that - this script is run at least once every two weeks. - */ - public static async void accessTokenRequest(string verificationCode) - { - var values = new Dictionary - { - { "client_id", clientId }, - { "client_secret", clientSecret }, - { "code", verificationCode }, - { "grant_type", "authorization_code"} - }; - - var content = new FormUrlEncodedContent(values); - var response = await _httpClient.PostAsync(tokenUrl, content); - var contents = await response.Content.ReadAsStringAsync(); - - accessToken = JsonConvert.DeserializeObject(contents); - - storeAccessToken(); - } - - /* - Returns a request message usable by httpClient for the purpose of a simple GET request - using a valid access token. - */ - public static HttpRequestMessage ApiGetRequest(string uri) - { - return new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(baseUri + uri), - Headers = { - { HttpRequestHeader.Authorization.ToString(), String.Format("Bearer {0}", accessToken.access_token)}, - { HttpRequestHeader.Accept.ToString(), "application/json" }, - } - }; - } - - /* - Example of an API request - in this case invoking api.projectplace.com/1/user/me/profile which returns - basic data about a user. - */ - public static async Task profileRequest() - { - var httpRequestMessage = ApiGetRequest("/1/user/me/profile"); - - var response = await _httpClient.SendAsync(httpRequestMessage); - if (response.IsSuccessStatusCode) { - var responseContents = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(responseContents); - } - - return null; - } - - /* - Script entry point - */ - public static void Main(string[] args) - { - ensureAccessToken(); - - /* In the first run - you will not have an access token stored - and will always end up in this if-statement */ - if (accessToken is null) { - string randomState = rand.Next(999999999).ToString(); - string readyAuthUrl = String.Format("{0}?client_id={1}&state={2}&redirect_uri={3}", authUrl, clientId, randomState, redirectUri); - Console.WriteLine("Authorize this application by going to {0}", readyAuthUrl); - Console.WriteLine("Enter verification code here (hint: look in the address bar of the browser for the code parameter):"); - string verificationCode = Console.ReadLine(); - accessTokenRequest(verificationCode); - } - Profile profile = profileRequest().Result; - Console.WriteLine("Successfully fetched profile for {0} {1} ({2})", profile.first_name, profile.last_name, profile.email); - } - } -} \ No newline at end of file diff --git a/oauth2/node.js b/oauth2/node.js deleted file mode 100644 index da6b7d1..0000000 --- a/oauth2/node.js +++ /dev/null @@ -1,120 +0,0 @@ -/* -This script show cases the OAuth2 flow using Node JS. -To test the script you must first have an application registered with ProjectPlace. -You also need to have the "simple-oauth2", "prompt.sync" libraries installed. -Run the script with: - node .\node.js -The first thing that will happen is that the script will ask you for your Client ID and Client Secret, -They may look like this 0833dea4f3ffffff1e6295ac1b3d3e08 deded29dba64fffffa20aa4acae81166addda836. -Then it will ask you for your application redirect URI. -In the console you will then see a link, open this link in your web browser, this will prompt you to authenticate the application. -You will then be redirected to the redirect URI, in the URI there will be a code attribute. -Copy that attribute to the terminal as prompted. -The script will test and see if the Token is valid by calling the profile API. -Now you should have a file called ".access_token.json" with your access Token inside. - -NOTE: -This code needs to run on Node JS version 18 or higher that supports the Fetch API. -You might get this in the Terminal: - ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time - (Use `node --trace-warnings ...` to show where the warning was created) -This is normal. -*/ - -const { AuthorizationCode } = require('simple-oauth2') -const prompt = require('prompt-sync')(); -const FS = require("fs"); - -const accessToken_Headers = new Headers(); - -const config = { - client: { - id: prompt("Client ID: "), - secret: prompt("Client Secret: ") - }, - auth: { - tokenHost: 'https://api.projectplace.com', - tokenPath: '/oauth2/access_token', - authorizeHost: 'https://api.projectplace.com', - authorizePath: '/oauth2/authorize' - } -} - -async function refresh_Token(accessTokenFileContents) { - if ("refresh_token" in JSON.parse(accessTokenFileContents)) { - const client = new AuthorizationCode(config); - - let accessToken = client.createToken(JSON.parse(accessTokenFileContents)); - if (accessToken.expired()) { - accessToken = await accessToken.refresh(); - checkAccessToken(accessToken); - } else { - refresh_question = prompt("Token has not expired, do you wish to renew it either way? y/n: "); - refresh_question = refresh_question.toLowerCase(); - if (refresh_question == "n") { return false} - console.log("Before Refresh:", accessToken); - - try { - accessToken = await accessToken.refresh(); - } catch (error) { - console.log(error); - } - console.log("After refresh:", accessToken) - checkAccessToken(accessToken); - } - } else { - console.log("Missing 'refresh_token' in .access_token.json - creating a new Token"); - get_Access_Token() - } -} - -async function get_Access_Token() { - const client = new AuthorizationCode(config); - - const redirect_uri = prompt("Application redirect URI: ") - - const authorizationUri = client.authorizeURL({ - redirect_uri: redirect_uri, - state: '' - }); - - console.log('Go to this URL and authorize the app:') - console.log(authorizationUri) - - const authorizationCode = prompt('Paste the authorization code here: '); - - const tokenParams = { - code: authorizationCode, - redirect_uri: redirect_uri, - }; - const accessToken = await client.getToken(tokenParams); - checkAccessToken(accessToken) -} - -async function checkAccessToken(accessToken) { - accessToken_Headers.append("Authorization", "Bearer " + accessToken.token.access_token); - const response = await fetch("https://api.projectplace.com/1/user/me/profile", {"headers": accessToken_Headers}) - .then((data) => { - console.log("Access token seems valid, saving to -> acess_token.json"); - console.log("Access token: ", accessToken.token.access_token); - FS.writeFileSync(".access_token.json", JSON.stringify(accessToken)); - }) - .catch((data) => { - console.log(data); - }) - - return response; -} - -async function run() { - try { - let accessTokenFileContents = FS.readFileSync(".access_token.json"); - await refresh_Token(accessTokenFileContents); - } catch (error) { - if (error.code === "ENOENT") { - await get_Access_Token(); - } - } -} - -run(); diff --git a/oauth2/package.json b/oauth2/package.json deleted file mode 100644 index c97954a..0000000 --- a/oauth2/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "node-oauth2", - "engines": { - "node": ">=18.0.0" - }, - "version": "1.0.0", - "description": "OAuth2 Node JS example", - "main": "node.js", - "author": "Planview ProjectPlace", - "license": "ISC", - "dependencies": { - "node-fetch": "^3.3.1", - "prompt-sync": "^4.2.0", - "simple-oauth2": "^5.0.0" - } -} diff --git a/oauth2/python.py b/oauth2/python.py deleted file mode 100644 index 9935702..0000000 --- a/oauth2/python.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -This script show cases the OAuth2 flow using Python 3 - -To test the script you must first have an application registered with Projectplace. - -You also need to have the `requests` and `requests_oauthlib` libraries installed. - -Invoke the script as such: - - $ python python.py CLIENT_ID APPLICATION_SECRET REDIRECT_URI - -For example like this - - $ python python.py 0833dea4f3ffffff1e6295ac1b3d3e08 deded29dba64fffffa20aa4acae81166addda836 https://lvh.me/myredirect - -The first thing that will happen is that a browser window will open, prompting you to authenticate the application. - -Once you have done that, you will be redirected to the redirect URI and in the URI there will be a code attribute, -copy that attribute to the terminal as prompted. - -Now you have an access token, and the script demos how you can call the profile API. - -You can test how to refresh an access token by supplying the optional parameters `--refresh` and you can -go through the authentication flow again by supplying `--reauth` -""" -import pprint -import webbrowser -import requests -import argh -import json -import os -from requests_oauthlib import OAuth2Session - -authorization_base_url = 'https://api.projectplace.com/oauth2/authorize' -token_url = 'https://api.projectplace.com/oauth2/access_token' -api_endpoint = 'https://api.projectplace.com' - - -def get_authorized_session(session, client_id, client_secret): - authorization_url, state = session.authorization_url(authorization_base_url) - - print('Opening webrowser to', authorization_url) - webbrowser.open(authorization_url, new=1) - oauth_code = input('Enter Code: ') - payload = { - 'client_id': client_id, - 'client_secret': client_secret, - 'code': oauth_code, - 'grant_type': 'authorization_code' - } - access_token_response = requests.post(token_url, data=payload) - - if access_token_response.status_code == 200: - token = access_token_response.json() - - print('User successfully authorized, with token:', token) - - with open('access_token.json', 'w') as stored_access_token: - stored_access_token.write(json.dumps(token)) - - return OAuth2Session(client_id=client_id, token=token) - - else: - print(access_token_response.text) - - -@argh.arg('--auth', help='Supply this flag in order to go through the entire flow from the start') -@argh.arg('--refresh', help='Supply this flag in order to refresh an already existing access token') -@argh.arg('redirect_uri', help='This is the redirect (callback) URL as defined in your application settings (this must match precisely)') -@argh.arg('client_secret', help='This is the secret of your application') -@argh.arg('client_id', help='This is the ID of your application') -def do_authorization_flow( - client_id: str, client_secret: str, redirect_uri: str, refresh: bool = False, - auth: bool = False -): - token = None - if os.path.isfile('access_token.json'): - with open('access_token.json', 'r') as stored_access_token: - try: - token = json.loads(stored_access_token.read()) - except ValueError: - pass - - if token and not auth: - projectplace = OAuth2Session(client_id, token=token) - else: - projectplace = get_authorized_session( - OAuth2Session(client_id, redirect_uri=redirect_uri), client_id, client_secret - ) - - print('Calling with token', projectplace.token) - response = projectplace.get(api_endpoint + '/1/user/me/profile') - - if response.status_code == 401 or refresh: - print('Attempting to refresh token.') - - refresh_response = requests.post(token_url, { - 'client_id': client_id, - 'client_secret': client_secret, - 'refresh_token': projectplace.token[u'refresh_token'], - 'grant_type': 'refresh_token' - }) - - if refresh_response.status_code == 200: - token = refresh_response.json() - with open('access_token.json', 'w') as stored_access_token: - stored_access_token.write(json.dumps(token)) - print('Refreshing token worked, new access token:', token) - else: - print('Refreshing failed, response =', refresh_response.text) - - if response.status_code == 200: - print('200 OK Successfully fetched profile belonging to', response.json()['sort_name']) - - -if __name__ == '__main__': - argh.dispatch_command(do_authorization_flow) diff --git a/oauth2/ruby.rb b/oauth2/ruby.rb deleted file mode 100644 index 414f94d..0000000 --- a/oauth2/ruby.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'launchy' -require 'oauth2' -require 'net/http' -require 'uri' -require 'json' - -client_id = 'ENTER_CLIENT_ID_HERE' -client_secret = 'ENTER_CLIENT_SECRET_HERE' -redirect_url = 'ENTER_REDIRECT_URL_AS_SPECIFIED_IN_APP_HERE' -api_endpoint = 'https://api.projectplace.com' -authorize_url = '/oauth2/authorize' # Relative to api_endpoint -token_url = '/oauth2/access_token' - -client = OAuth2::Client.new(client_id, client_secret, :site => api_endpoint) - -# Open a webbrowser to start the authorisation process. -Launchy.open(api_endpoint + authorize_url + '?client_id=' + client_id + '&redrect_url=' + redirect_url) - -# Once completed the browser will redirect, grab the "code" parameter from the URL and enter here -# Normally you would end up in your own callback where you can grab the "code" programatically -puts "Enter code" - -code = gets.chomp - -# Request access token -response = Net::HTTP.post_form(URI.parse(api_endpoint + token_url), { - "client_id" => client_id, - "client_secret" => client_secret, - "code" => code, - "grant_type" => "authorization_code" -}) - -token_response = JSON.parse(response.body) - -token = OAuth2::AccessToken.from_hash(client, token_response) - -# Issue an API request using the access token, and pretty print it. -profile_response = token.get('/1/user/me/projects') -puts JSON.pretty_generate(JSON.parse(profile_response.body)) -