Skip to content

Commit 44479fe

Browse files
committed
Penneo Certified Connectors
1 parent afab2cc commit 44479fe

File tree

18 files changed

+2286
-0
lines changed

18 files changed

+2286
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Penneo Pluto - Sandbox OAuth API Connector
2+
3+
Penneo provides a powerful REST API for digital signing and document management. Using this API, you can obtain JWT access tokens for authenticating with Penneo's integration APIs. Very often, you may want to leverage Penneo's authentication capabilities in your application, or in your process automation to securely access Penneo services.
4+
5+
## Publisher: Penneo
6+
7+
## Prerequisites
8+
9+
To use this connector, you will need:
10+
11+
- A Penneo sandbox account with admin access
12+
- Client ID and Client Secret from Penneo Support
13+
- Company API Key and API Secret from your Penneo account
14+
15+
## Supported Operations
16+
17+
### Get an access token
18+
Exchanges API keys for a JWT access token that can be used to authenticate with Penneo's integration APIs. This operation implements Penneo's custom OAuth flow using API keys, which exchanges your client credentials and API keys for a JWT access token.
19+
20+
## Obtaining Credentials
21+
22+
Penneo OAuth APIs use a custom authentication flow with API keys. You need to obtain the necessary credentials from Penneo to identify your connector to Penneo's OAuth service so that it can exchange credentials for a JWT access token. You can read more about this here: https://penneo.readme.io/docs/using-oauth#api-keys-grant
23+
24+
### Step 1: Obtain Client ID and Client Secret
25+
26+
1. **Contact Penneo Support**: Request OAuth client credentials for your application
27+
- Email: [email protected] (or use your Penneo support channel / https://www.support.penneo.com/hc/en-gb/requests/new)
28+
- Request: "I need OAuth client credentials (client_id and client_secret) for my Power Platform connector integration"
29+
- Provide details about your use case and integration requirements
30+
31+
2. **Receive Credentials**: Penneo Support will provide you with:
32+
- `client_id`: Your OAuth client identifier
33+
- `client_secret`: Your OAuth client secret (keep this secure)
34+
35+
### Step 2: Enable and Obtain API Keys
36+
37+
1. **Log in to Penneo Sandbox**:
38+
- Navigate to https://sandbox.penneo.com
39+
- Log in with your company admin account
40+
41+
2. **Enable API Keys**:
42+
- Go to Settings → Profile → User Details
43+
- Or navigate directly to: https://sandbox.penneo.com/settings/profile/user/details
44+
- Enable API Keys feature and refresh the page (if not already enabled)
45+
- Note: You must be a company administrator to enable API keys
46+
47+
3. **Retrieve API Credentials**:
48+
- From the API Keys tab, copy your:
49+
- `api_key`: Your company API key
50+
- `api_secret`: Your company API secret (keep this secure)
51+
52+
## Getting Started
53+
54+
### Obtaining an Access Token
55+
56+
1. **Setup Connection**:
57+
- Create a new connection to "Penneo Pluto Authenticator"
58+
- Enter all four required credentials:
59+
- Client ID
60+
- Client Secret
61+
- API Key
62+
- API Secret
63+
64+
2. **Call the Operation**:
65+
- Use the "Get an access token" operation
66+
- The connector will automatically:
67+
- Generate the required nonce and digest
68+
- Format the request correctly
69+
- Exchange credentials for a token
70+
71+
3. **Use the Token**:
72+
- Extract the `access_token` from the response
73+
- Use this JWT token in the `X-Auth-Token` header when calling other Penneo APIs
74+
- Note the `expires_in` value to know when to refresh the token
75+
76+
## How the OAuth Flow Works
77+
78+
This connector implements Penneo's custom OAuth flow as described on https://penneo.readme.io/docs/using-oauth#api-keys-grant:
79+
80+
1. **Input**: Receives `client_id`, `client_secret`, `api_key`, and `api_secret`
81+
82+
2. **Digest Generation (WSSE)**:
83+
- Generates a random 8-byte nonce
84+
- Creates a timestamp in ISO 8601 format
85+
- Concatenates: nonce + timestamp + api_secret
86+
- Computes SHA-1 hash of the concatenated bytes
87+
- Base64 encodes the hash to create the digest
88+
89+
3. **Token Request**:
90+
- Sends POST request to `https://sandbox.oauth.penneo.cloud/oauth/token`
91+
- Request body includes:
92+
- `client_id`
93+
- `client_secret`
94+
- `grant_type`: "api_keys"
95+
- `key`: api_key
96+
- `nonce`: Generated nonce
97+
- `created_at`: Timestamp
98+
- `digest`: Generated digest
99+
100+
4. **Response**: Returns a JWT access token with:
101+
- `access_token`: The JWT token for API authentication
102+
- `token_type`: "Bearer"
103+
- `expires_in`: Token lifetime in seconds
104+
- `access_token_expires_at`: Unix timestamp of expiration
105+
106+
## Known Issues and Limitations
107+
108+
1. **Token Expiration**: Access tokens expire(currently every 600s). Monitor the `expires_in` or `access_token_expires_at` fields and refresh tokens before expiration.
109+
110+
2. **Sandbox Environment**: This connector is configured for the Penneo sandbox environment (`sandbox.oauth.penneo.cloud`).
111+
112+
3. **Digest Algorithm**: WSSE - the connector uses SHA-1 for digest generation as required by Penneo's API. This is a requirement of the Penneo API specification.
113+
114+
4. **Credential Security**:
115+
- Never hardcode credentials in your flows or apps
116+
- Use secure connection storage provided by Power Platform
117+
- Rotate credentials if compromised
118+
119+
5. **Nonce Generation**: Each token request generates a unique nonce. The connector handles this automatically.
120+
121+
## Security Considerations
122+
123+
- **Client Secret and API Secret**: These are sensitive credentials. Store them securely in Power Platform connection settings, never in code or logs.
124+
- **Token Storage**: Access tokens should be stored securely and not logged or exposed.
125+
126+
## Deployment Instructions
127+
128+
Run the following commands and follow the prompts:
129+
130+
```
131+
paconn create --api-def apiDefinition.swagger.json --api-prop apiProperties.json --icon icon.png --script script.csx
132+
```
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"title": "Penneo Pluto Authenticator Sandbox",
5+
"description": "Penneo Pluto - API for Penneo Sandbox OAuth",
6+
"version": "1.0.0",
7+
"contact": {
8+
"name": "Penneo Support",
9+
"email": "[email protected]"
10+
}
11+
},
12+
"x-ms-connector-metadata": [
13+
{
14+
"propertyName": "source",
15+
"propertyValue": "Swagger"
16+
},
17+
{
18+
"propertyName": "Website",
19+
"propertyValue": "https://penneo.com"
20+
},
21+
{
22+
"propertyName": "Privacy policy",
23+
"propertyValue": "https://penneo.com/privacy-policy"
24+
},
25+
{
26+
"propertyName": "Categories",
27+
"propertyValue": "Productivity"
28+
}
29+
],
30+
"host": "sandbox.oauth.penneo.cloud",
31+
"basePath": "/",
32+
"schemes": [
33+
"https"
34+
],
35+
"paths": {
36+
"/oauth/token": {
37+
"post": {
38+
"summary": "Penneo Pluto Sandbox Get an access token",
39+
"description": "Exchanges API keys for an access token.",
40+
"operationId": "getAccessToken",
41+
"consumes": [
42+
"application/json"
43+
],
44+
"produces": [
45+
"application/json"
46+
],
47+
"parameters": [
48+
{
49+
"in": "body",
50+
"name": "body",
51+
"description": "API key credentials",
52+
"required": true,
53+
"schema": {
54+
"type": "object",
55+
"properties": {
56+
"client_id": {
57+
"type": "string",
58+
"description": "Your Penneo client ID. Request one from Penneo Support"
59+
},
60+
"client_secret": {
61+
"type": "string",
62+
"description": "Your Penneo client secret.",
63+
"x-ms-secret": true
64+
},
65+
"api_key": {
66+
"type": "string",
67+
"description": "Your company API Key. As company admin, enable API Keys and get it from the https://sandbox.penneo.com/settings/profile/user/details"
68+
},
69+
"api_secret": {
70+
"type": "string",
71+
"description": "Your company API secret, used for generating the digest.",
72+
"x-ms-secret": true
73+
}
74+
},
75+
"required": [
76+
"client_id",
77+
"client_secret",
78+
"api_key",
79+
"api_secret"
80+
]
81+
}
82+
}
83+
],
84+
"responses": {
85+
"200": {
86+
"description": "Successful response with an access token.",
87+
"schema": {
88+
"type": "object",
89+
"properties": {
90+
"access_token": {
91+
"type": "string",
92+
"description": "The JWT access token you can use further to call the integrations API."
93+
},
94+
"token_type": {
95+
"type": "string",
96+
"description": "The type of the token, \"Bearer\""
97+
},
98+
"expires_in": {
99+
"type": "integer",
100+
"format": "int32",
101+
"description": "The lifetime of the access token in seconds."
102+
},
103+
"access_token_expires_at": {
104+
"type": "integer",
105+
"format": "int32",
106+
"description": "The Unix timestamp when the access token will expire."
107+
}
108+
}
109+
}
110+
},
111+
"400": {
112+
"description": "Invalid request."
113+
}
114+
}
115+
}
116+
}
117+
},
118+
"securityDefinitions": {}
119+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"properties": {
3+
"connectionParameters": {},
4+
"iconBrandColor": "#005696",
5+
"capabilities": [],
6+
"publisher": "Penneo"
7+
}
8+
}
693 Bytes
Loading
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
10+
public class Script : ScriptBase
11+
{
12+
// This method is the entry point for the custom connector's logic.
13+
public override async Task<HttpResponseMessage> ExecuteAsync()
14+
{
15+
Context.Logger.LogTrace("Starting custom connector script execution.");
16+
17+
// 1. Retrieve the required inputs from the connector context by reading the request body.
18+
string requestBody = await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false);
19+
Context.Logger.LogTrace($"Received request body: {requestBody}");
20+
21+
JObject jsonBody = JObject.Parse(requestBody);
22+
23+
string clientId = jsonBody["client_id"]?.ToString() ?? string.Empty;
24+
string clientSecret = jsonBody["client_secret"]?.ToString() ?? string.Empty;
25+
string apiKey = jsonBody["api_key"]?.ToString() ?? string.Empty;
26+
string apiSecret = jsonBody["api_secret"]?.ToString() ?? string.Empty;
27+
28+
Context.Logger.LogTrace($"Extracted parameters: client_id='{clientId}', client_secret='{clientSecret}', api_key='{apiKey}', api_secret='{apiSecret}'");
29+
30+
// 2. Generate the nonce, created timestamp, and digest as per the PHP example.
31+
byte[] rawNonce = new byte[8];
32+
using (var rng = RandomNumberGenerator.Create())
33+
{
34+
rng.GetBytes(rawNonce);
35+
}
36+
37+
string nonce = Convert.ToBase64String(rawNonce);
38+
string created = DateTime.UtcNow.ToString("yyyy-MM-dd\\THH:mm:ss.fff\\Z");
39+
40+
Context.Logger.LogTrace($"Generated nonce and timestamp: nonce='{nonce}', created='{created}'");
41+
42+
byte[] createdBytes = Encoding.UTF8.GetBytes(created);
43+
byte[] apiSecretBytes = Encoding.UTF8.GetBytes(apiSecret);
44+
45+
byte[] concatenatedBytes = new byte[rawNonce.Length + createdBytes.Length + apiSecretBytes.Length];
46+
Buffer.BlockCopy(rawNonce, 0, concatenatedBytes, 0, rawNonce.Length);
47+
Buffer.BlockCopy(createdBytes, 0, concatenatedBytes, rawNonce.Length, createdBytes.Length);
48+
Buffer.BlockCopy(apiSecretBytes, 0, concatenatedBytes, rawNonce.Length + createdBytes.Length, apiSecretBytes.Length);
49+
50+
byte[] digestBytes;
51+
using (var sha1 = SHA1.Create())
52+
{
53+
digestBytes = sha1.ComputeHash(concatenatedBytes);
54+
}
55+
56+
string digest = Convert.ToBase64String(digestBytes);
57+
58+
Context.Logger.LogTrace($"Generated digest: {digest}");
59+
60+
// 3. Construct the request body as a JSON object.
61+
var requestBodyPayload = new
62+
{
63+
client_id = clientId,
64+
client_secret = clientSecret,
65+
grant_type = "api_keys",
66+
key = apiKey,
67+
nonce = nonce,
68+
created_at = created,
69+
digest = digest
70+
};
71+
72+
// 4. Create the JSON content to be sent in the request.
73+
string jsonPayload = JsonConvert.SerializeObject(requestBodyPayload);
74+
Context.Logger.LogTrace($"Final payload to send: {jsonPayload}");
75+
var content = CreateJsonContent(jsonPayload);
76+
77+
// 5. Create the full URI for the POST request.
78+
string requestUri = "https://sandbox.oauth.penneo.cloud/oauth/token";
79+
80+
// 6. Create the HttpRequestMessage with the correct method and content.
81+
var request = new HttpRequestMessage(HttpMethod.Post, requestUri)
82+
{
83+
Content = content
84+
};
85+
86+
// 7. Send the request using the HttpClient instance provided by the base class.
87+
HttpResponseMessage response = await Context.SendAsync(request, CancellationToken);
88+
Context.Logger.LogInformation($"HTTP request sent. Response status code: {response.StatusCode}");
89+
90+
// 8. Return the HTTP response.
91+
return response;
92+
}
93+
}

0 commit comments

Comments
 (0)