Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions certified-connectors/Penneo Auth Sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Penneo - Authentication (Sand)

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.

## Publisher: Penneo

## Prerequisites

To use this connector, you will need:

- A Penneo sandbox account with admin access
- Client ID and Client Secret from Penneo Support
- Company API Key and API Secret from your Penneo account

## Supported Operations

### Get an access token
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.

## Obtaining Credentials

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

### Step 1: Obtain Client ID and Client Secret

1. **Contact Penneo Support**: Request OAuth client credentials for your application
- Email: [email protected] (or use your Penneo support channel / https://www.support.penneo.com/hc/en-gb/requests/new)
- Request: "I need OAuth client credentials (client_id and client_secret) for my Power Platform connector integration"
- Provide details about your use case and integration requirements

2. **Receive Credentials**: Penneo Support will provide you with:
- `client_id`: Your OAuth client identifier
- `client_secret`: Your OAuth client secret (keep this secure)

### Step 2: Enable and Obtain API Keys

1. **Log in to Penneo Sandbox**:
- Navigate to https://sandbox.penneo.com
- Log in with your company admin account

2. **Enable API Keys**:
- Go to Settings → Profile → User Details
- Or navigate directly to: https://sandbox.penneo.com/settings/profile/user/details
- Enable API Keys feature and refresh the page (if not already enabled)
- Note: You must be a company administrator to enable API keys

3. **Retrieve API Credentials**:
- From the API Keys tab, copy your:
- `api_key`: Your company API key
- `api_secret`: Your company API secret (keep this secure)

## Getting Started

### Obtaining an Access Token

1. **Setup Connection**:
- Create a new connection to "Penneo Authenticator"
- Enter all four required credentials:
- Client ID
- Client Secret
- API Key
- API Secret

2. **Call the Operation**:
- Use the "Get an access token" operation
- The connector will automatically:
- Generate the required nonce and digest
- Format the request correctly
- Exchange credentials for a token

3. **Use the Token**:
- Extract the `access_token` from the response
- Use this JWT token in the `X-Auth-Token` header when calling other Penneo APIs
- Note the `expires_in` value to know when to refresh the token

## How the OAuth Flow Works

This connector implements Penneo's custom OAuth flow as described on https://penneo.readme.io/docs/using-oauth#api-keys-grant:

1. **Input**: Receives `client_id`, `client_secret`, `api_key`, and `api_secret`

2. **Digest Generation (WSSE)**:
- Generates a random 8-byte nonce
- Creates a timestamp in ISO 8601 format
- Concatenates: nonce + timestamp + api_secret
- Computes SHA-1 hash of the concatenated bytes
- Base64 encodes the hash to create the digest

3. **Token Request**:
- Sends POST request to `https://sandbox.oauth.penneo.cloud/oauth/token`
- Request body includes:
- `client_id`
- `client_secret`
- `grant_type`: "api_keys"
- `key`: api_key
- `nonce`: Generated nonce
- `created_at`: Timestamp
- `digest`: Generated digest

4. **Response**: Returns a JWT access token with:
- `access_token`: The JWT token for API authentication
- `token_type`: "Bearer"
- `expires_in`: Token lifetime in seconds
- `access_token_expires_at`: Unix timestamp of expiration

## Known Issues and Limitations

1. **Token Expiration**: Access tokens expire(currently every 600s). Monitor the `expires_in` or `access_token_expires_at` fields and refresh tokens before expiration.

2. **Sandbox Environment**: This connector is configured for the Penneo sandbox environment (`sandbox.oauth.penneo.cloud`).

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.

4. **Credential Security**:
- Never hardcode credentials in your flows or apps
- Use secure connection storage provided by Power Platform
- Rotate credentials if compromised

5. **Nonce Generation**: Each token request generates a unique nonce. The connector handles this automatically.

## Security Considerations

- **Client Secret and API Secret**: These are sensitive credentials. Store them securely in Power Platform connection settings, never in code or logs.
- **Token Storage**: Access tokens should be stored securely and not logged or exposed.

## Deployment Instructions

Run the following commands and follow the prompts:

```
paconn create --api-def apiDefinition.swagger.json --api-prop apiProperties.json --icon icon.png --script script.csx
```
119 changes: 119 additions & 0 deletions certified-connectors/Penneo Auth Sandbox/apiDefinition.swagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"swagger": "2.0",
"info": {
"title": "Penneo - Authentication (Sand)",
"description": "Penneo - Authentication (sandbox) - Used to obtain an access token for the Penneo API.",
"version": "1.0.0",
"contact": {
"name": "Penneo Support",
"email": "[email protected]"
}
},
"x-ms-connector-metadata": [
{
"propertyName": "source",
"propertyValue": "Swagger"
},
{
"propertyName": "Website",
"propertyValue": "https://penneo.com"
},
{
"propertyName": "Privacy policy",
"propertyValue": "https://penneo.com/privacy-policy"
},
{
"propertyName": "Categories",
"propertyValue": "Productivity"
}
],
"host": "sandbox.oauth.penneo.cloud",
"basePath": "/",
"schemes": [
"https"
],
"paths": {
"/oauth/token": {
"post": {
"summary": "Penneo Sandbox Get an access token",
"description": "Exchanges API keys for an access token.",
"operationId": "getAccessToken",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "API key credentials",
"required": true,
"schema": {
"type": "object",
"properties": {
"client_id": {
"type": "string",
"description": "Your Penneo client ID. Request one from Penneo Support"
},
"client_secret": {
"type": "string",
"description": "Your Penneo client secret.",
"x-ms-secret": true
},
"api_key": {
"type": "string",
"description": "Your company API Key. As company admin, enable API Keys and get it from the https://sandbox.penneo.com/settings/profile/user/details"
},
"api_secret": {
"type": "string",
"description": "Your company API secret, used for generating the digest.",
"x-ms-secret": true
}
},
"required": [
"client_id",
"client_secret",
"api_key",
"api_secret"
]
}
}
],
"responses": {
"200": {
"description": "Successful response with an access token.",
"schema": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
"description": "The JWT access token you can use further to call the integrations API."
},
"token_type": {
"type": "string",
"description": "The type of the token, \"Bearer\""
},
"expires_in": {
"type": "integer",
"format": "int32",
"description": "The lifetime of the access token in seconds."
},
"access_token_expires_at": {
"type": "integer",
"format": "int32",
"description": "The Unix timestamp when the access token will expire."
}
}
}
},
"400": {
"description": "Invalid request."
}
}
}
}
},
"securityDefinitions": {}
}
8 changes: 8 additions & 0 deletions certified-connectors/Penneo Auth Sandbox/apiProperties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"properties": {
"connectionParameters": {},
"iconBrandColor": "#005696",
"capabilities": [],
"publisher": "Penneo"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions certified-connectors/Penneo Auth Sandbox/script.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Script : ScriptBase
{
// This method is the entry point for the custom connector's logic.
public override async Task<HttpResponseMessage> ExecuteAsync()
{
Context.Logger.LogTrace("Starting custom connector script execution.");

// 1. Retrieve the required inputs from the connector context by reading the request body.
string requestBody = await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false);
Context.Logger.LogTrace($"Received request body: {requestBody}");

JObject jsonBody = JObject.Parse(requestBody);

string clientId = jsonBody["client_id"]?.ToString() ?? string.Empty;
string clientSecret = jsonBody["client_secret"]?.ToString() ?? string.Empty;
string apiKey = jsonBody["api_key"]?.ToString() ?? string.Empty;
string apiSecret = jsonBody["api_secret"]?.ToString() ?? string.Empty;

Context.Logger.LogTrace($"Extracted parameters: client_id='{clientId}', client_secret='{clientSecret}', api_key='{apiKey}', api_secret='{apiSecret}'");

// 2. Generate the nonce, created timestamp, and digest as per the PHP example.
byte[] rawNonce = new byte[8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(rawNonce);
}

string nonce = Convert.ToBase64String(rawNonce);
string created = DateTime.UtcNow.ToString("yyyy-MM-dd\\THH:mm:ss.fff\\Z");

Context.Logger.LogTrace($"Generated nonce and timestamp: nonce='{nonce}', created='{created}'");

byte[] createdBytes = Encoding.UTF8.GetBytes(created);
byte[] apiSecretBytes = Encoding.UTF8.GetBytes(apiSecret);

byte[] concatenatedBytes = new byte[rawNonce.Length + createdBytes.Length + apiSecretBytes.Length];
Buffer.BlockCopy(rawNonce, 0, concatenatedBytes, 0, rawNonce.Length);
Buffer.BlockCopy(createdBytes, 0, concatenatedBytes, rawNonce.Length, createdBytes.Length);
Buffer.BlockCopy(apiSecretBytes, 0, concatenatedBytes, rawNonce.Length + createdBytes.Length, apiSecretBytes.Length);

byte[] digestBytes;
using (var sha1 = SHA1.Create())
{
digestBytes = sha1.ComputeHash(concatenatedBytes);
}

string digest = Convert.ToBase64String(digestBytes);

Context.Logger.LogTrace($"Generated digest: {digest}");

// 3. Construct the request body as a JSON object.
var requestBodyPayload = new
{
client_id = clientId,
client_secret = clientSecret,
grant_type = "api_keys",
key = apiKey,
nonce = nonce,
created_at = created,
digest = digest
};

// 4. Create the JSON content to be sent in the request.
string jsonPayload = JsonConvert.SerializeObject(requestBodyPayload);
Context.Logger.LogTrace($"Final payload to send: {jsonPayload}");
var content = CreateJsonContent(jsonPayload);

// 5. Create the full URI for the POST request.
string requestUri = "https://sandbox.oauth.penneo.cloud/oauth/token";

// 6. Create the HttpRequestMessage with the correct method and content.
var request = new HttpRequestMessage(HttpMethod.Post, requestUri)
{
Content = content
};

// 7. Send the request using the HttpClient instance provided by the base class.
HttpResponseMessage response = await Context.SendAsync(request, CancellationToken);
Context.Logger.LogInformation($"HTTP request sent. Response status code: {response.StatusCode}");

// 8. Return the HTTP response.
return response;
}
}
Loading