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/auth/py-oauth1/requirements.txt b/auth/py-oauth1/requirements.txt new file mode 100644 index 0000000..cbbb937 --- /dev/null +++ b/auth/py-oauth1/requirements.txt @@ -0,0 +1,2 @@ +requests +requests-oauthlib 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/auth/py-oauth2-authorization-code/requirements.txt b/auth/py-oauth2-authorization-code/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/auth/py-oauth2-authorization-code/requirements.txt @@ -0,0 +1 @@ +requests 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/auth/py-oauth2-client-credentials/requirements.txt b/auth/py-oauth2-client-credentials/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/auth/py-oauth2-client-credentials/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/examples/readme.md b/examples/readme.md deleted file mode 100644 index 764edfc..0000000 --- a/examples/readme.md +++ /dev/null @@ -1,15 +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. - -Some clarifications: - -* 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. - 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)) -