Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ dependencies at [getcomposer.org](http://getcomposer.org).

This section documents the different objects available through the SDK and how to use them.

For **runnable CLI demos** (case file E2E script and OAuth sample scripts), see [docs/cli-examples.md](docs/cli-examples.md).

### Authentication

The SDK supports three different methods of authentication:
Expand Down Expand Up @@ -153,7 +155,7 @@ connector using the already authorized `$oAuth` instance:
Penneo\SDK\ApiConnector::initializeOAuth($oAuth);
```

> :point_right: see a full, functional example in [docs/interactive_oauth_example.php](docs/interactive_oauth_example.php).
> :point_right: see a full, configurable example in [docs/interactive_oauth_example.php](docs/interactive_oauth_example.php) and **localhost setup** in [docs/cli-examples.md](docs/cli-examples.md#interactive-oauth-on-localhost-step-by-step).

##### OAuth Token Storage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* End-to-end demo: build a case file with the Penneo SDK (multiple entity types).
*
* Run from the repository root after `composer install`:
* php examples/casefile-e2e-demo.php
* php docs/casefile-e2e-demo.php
*
* Authentication (pick one):
* WSSE (default API v1 sandbox):
Expand Down
136 changes: 136 additions & 0 deletions docs/cli-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# CLI examples (Penneo SDK for PHP)

Runnable PHP scripts in this folder:

| Script | Purpose |
|--------|---------|
| [casefile-e2e-demo.php](casefile-e2e-demo.php) | Full flow: folder, case file, documents, signer, signing request, copy recipient, activate (see below). |
| [programmatic_oauth_example.php](programmatic_oauth_example.php) | OAuth without browser (server-side / no redirect). |
| [interactive_oauth_example.php](interactive_oauth_example.php) | OAuth with PKCE redirect flow (`?code=` callback). |

The root [README](../README.md) links to the OAuth examples from the authentication section.

### How to try the OAuth scripts

From the repository root (after `composer install`), replace the placeholder `clientId` / `clientSecret` / `apiKey` / `apiSecret` (and for interactive, `redirectUri` registered at Penneo), then:

```bash
# Programmatic — needs valid OAuth client + API key/secret; reaches API on CaseFile::persist()
php docs/programmatic_oauth_example.php
```

```bash
# Interactive — from repo or from docs/, `php interactive_oauth_example.php` prints the Penneo authorize URL
# (header() alone shows nothing in CLI). To complete the flow use a browser:
php -S 127.0.0.1:8080 -t docs
# Open http://127.0.0.1:8080/interactive_oauth_example.php — login — callback with ?code= runs persist()
```

Without real credentials, programmatic fails at token exchange (e.g. HTTP 401) and interactive still performs the redirect to Penneo’s authorize URL (verify in browser).

### Interactive OAuth on localhost (step by step)

Penneo sends the user back to an URL you register on the OAuth client. **That URL, the address bar in the browser, and `PENNEO_OAUTH_REDIRECT_URI` must be identical** (including `http` vs `https`, `localhost` vs `127.0.0.1`, port, path, and **`/interactive_oauth_example.php`** — the file extension is required).

1. In Penneo (sandbox), add a redirect URI such as:
`http://127.0.0.1:8080/interactive_oauth_example.php`

2. From the **repository root** (where `vendor/` lives):

```bash
composer install
export PENNEO_OAUTH_CLIENT_ID="your_client_id"
export PENNEO_OAUTH_CLIENT_SECRET="your_client_secret"
# Optional if you use localhost instead of 127.0.0.1 everywhere:
# export PENNEO_OAUTH_REDIRECT_URI="http://localhost:8080/interactive_oauth_example.php"

php -S 127.0.0.1:8080 -t docs
```

3. Open **exactly**:
`http://127.0.0.1:8080/interactive_oauth_example.php`
(not `…/interactive_oauth_example` without `.php` unless your server maps it)

4. Log in at Penneo; after redirect you should see plain text like `OK — Case file created. id=…`

**Typical failures**

| What you see | Cause |
|--------------|--------|
| `redirect_uri_mismatch` | Redirect URI in Penneo ≠ URL in browser or ≠ env var. |
| `Missing PKCE code_verifier` | Started flow in one browser/session, callback in another; use one tab, avoid clearing cookies mid-flow. |
| Blank or 404 | Wrong path: add `.php`; ensure `-t docs` and script is under `docs/`. |
| Prompt for env vars | Export `PENNEO_OAUTH_CLIENT_ID` / `PENNEO_OAUTH_CLIENT_SECRET` in the **same terminal** where you run `php -S` (child inherits env). |

## `casefile-e2e-demo.php`

Demo script: creates a **folder**, a **case file** with **annex document + signable document**, **signer**, **signature line**, **signing request**, **copy recipient**, then **activates** the case file (no `send()` — no automated outbound e-mail). It prints the **signing link** at the end.

### Prerequisites

From the repository root:

```bash
composer install
```

### Minimal run (WSSE, default sandbox)

The SDK defaults to `https://sandbox.penneo.com/api/v1/` unless you set a different `PENNEO_API_BASE`.

```bash
export PENNEO_WSSE_KEY="your_key"
export PENNEO_WSSE_SECRET="your_secret"

php docs/casefile-e2e-demo.php
```

Optional (reseller account, acting on behalf of a customer):

```bash
export PENNEO_WSSE_USER="12345" # Penneo customer id
```

### Programmatic OAuth (API v3)

Same kind of setup as in the main README (`client_id`, `client_secret`, API key + API secret).

```bash
export PENNEO_AUTH=oauth
export PENNEO_OAUTH_ENV=sandbox # or production
export PENNEO_CLIENT_ID="..."
export PENNEO_CLIENT_SECRET="..."
export PENNEO_API_KEY="..."
export PENNEO_API_SECRET="..."

php docs/casefile-e2e-demo.php
```

### Optional environment variables (demo)

| Variable | Purpose |
|----------|---------|
| `PENNEO_DEMO_PDF` | Path to your PDF; if unset, a minimal temporary PDF is generated. |
| `PENNEO_DEMO_SIGNER_EMAIL` | E-mail on the `SigningRequest` (default: demo placeholder). |
| `PENNEO_DEMO_COPY_EMAIL` | E-mail for the **CopyRecipient** (default: demo placeholder). |
| `PENNEO_API_BASE` | API base URL for WSSE (see Production below). |

### Sandbox → production

**WSSE:** use **production** credentials and the live API base, for example:

```bash
export PENNEO_API_BASE="https://app.penneo.com/api/v1/"
export PENNEO_WSSE_KEY="..."
export PENNEO_WSSE_SECRET="..."
```

Confirm the exact API path (`v1` or other) with Penneo for your account.

**OAuth:** set `PENNEO_OAUTH_ENV=production` and production credentials (client + keys). The SDK will use the production signing API host (`https://app.penneo.com` / `api/v3/` as configured).

Do not reuse sandbox keys or secrets in production.

### Troubleshooting

For clearer HTTP errors you can use `ApiConnector::throwExceptions(true)` in your code (the demo script already does). For production, `ApiConnector::setLogger(...)` helps capture request ids for support.
119 changes: 95 additions & 24 deletions docs/interactive_oauth_example.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
<?php

/**
* Interactive OAuth (PKCE): open this script in a browser after starting a local server.
*
* 1) Register this exact Redirect URI in Penneo (OAuth client config):
* http://127.0.0.1:8080/interactive_oauth_example.php
* (or http://localhost:8080/... — must match character-for-character what you open in the browser)
*
* 2) From repository root:
* export PENNEO_OAUTH_CLIENT_ID="..."
* export PENNEO_OAUTH_CLIENT_SECRET="..."
* php -S 127.0.0.1:8080 -t docs
*
* 3) Open in browser: http://127.0.0.1:8080/interactive_oauth_example.php
*
* Optional: PENNEO_OAUTH_REDIRECT_URI — if unset, defaults to 127.0.0.1 URL above.
* Optional: PENNEO_OAUTH_ENV=sandbox|production (default sandbox).
*/

declare(strict_types=1);

require_once dirname(__DIR__) . '/vendor/autoload.php';

use Penneo\SDK\ApiConnector;
use Penneo\SDK\CaseFile;
use Penneo\SDK\OAuth\Config\Environment;
Expand All @@ -10,60 +32,109 @@

session_start();

// set up where to store the tokens - either use the provided session storage
$tokenStorage = new SessionTokenStorage('optionalKeyToPlaceTokensInto');
function interactiveOauthFail(string $message): void
{
if (PHP_SAPI === 'cli') {
fwrite(STDERR, $message . PHP_EOL);
} else {
header('Content-Type: text/plain; charset=utf-8');
echo $message;
}
exit(1);
}

$clientId = getenv('PENNEO_OAUTH_CLIENT_ID') ?: '';
$clientSecret = getenv('PENNEO_OAUTH_CLIENT_SECRET') ?: '';
$redirectUri = getenv('PENNEO_OAUTH_REDIRECT_URI') ?: 'http://127.0.0.1:8080/interactive_oauth_example.php';
$environment = getenv('PENNEO_OAUTH_ENV') ?: Environment::SANDBOX;

if ($clientId === '' || $clientSecret === '') {
interactiveOauthFail(
"Set environment variables before running:\n"
. " export PENNEO_OAUTH_CLIENT_ID='...'\n"
. " export PENNEO_OAUTH_CLIENT_SECRET='...'\n"
. "Optional:\n"
. " export PENNEO_OAUTH_REDIRECT_URI='http://127.0.0.1:8080/interactive_oauth_example.php'\n"
. " export PENNEO_OAUTH_ENV=sandbox\n"
. "\n"
. 'The redirect URI must be registered identically in your Penneo OAuth client.'
);
}

if (!Environment::isSupported($environment)) {
interactiveOauthFail("PENNEO_OAUTH_ENV must be 'sandbox' or 'production'. Got: {$environment}");
}

// or build a custom one by implementing the interface
// $tokenStorage = new class implements \Penneo\SDK\OAuth\Tokens\TokenStorage {};
$tokenStorage = new SessionTokenStorage('optionalKeyToPlaceTokensInto');

$penneoOAuth = OAuthBuilder::start()
->setEnvironment(Environment::SANDBOX)
->setClientId('clientId') // <- the credentials provided by Penneo
->setClientSecret('clientSecret') // <-
->setRedirectUri('http://dev.php.local') // the exact URL you provided to Penneo
->setEnvironment($environment)
->setClientId($clientId)
->setClientSecret($clientSecret)
->setRedirectUri($redirectUri)
->setTokenStorage($tokenStorage)
->build();

if (isset($_GET['error'])) {
// something went wrong - handle the error
print_r($_GET['error']);
exit;
} elseif (isset($_GET['code'])) {
// we are returning with a code after authorization
$detail = $_GET['error_description'] ?? '';
header('Content-Type: text/plain; charset=utf-8');
echo 'OAuth error: ' . $_GET['error'] . ($detail !== '' ? "\n" . $detail : '');
exit(1);
}

if (isset($_GET['code'])) {
if (empty($_SESSION['code_verifier'])) {
interactiveOauthFail(
"Missing PKCE code_verifier in session. Open this URL first in the same browser (no private window switch):\n"
. $redirectUri
);
}
try {
$penneoOAuth->exchangeAuthCode($_GET['code'], $_SESSION['code_verifier']);
} catch (PenneoSdkRuntimeException $e) {
/// something went wrong - handle the error
print_r($e);
exit;
header('Content-Type: text/plain; charset=utf-8');
echo 'Token exchange failed: ' . $e->getMessage();
exit(1);
}

// optionally, handle the returned state
print_r($_GET['state']);
} elseif (!$penneoOAuth->isAuthorized()) {
// set up the code challenge
$pkce = new PKCE();
$codeVerifier = $pkce->getCodeVerifier();
$_SESSION['code_verifier'] = $codeVerifier;

try {
// build the redirect URL for authorization
$url = $penneoOAuth->buildRedirectUrl(
['full_access'],
$pkce->getCodeChallenge($codeVerifier)
);

if (PHP_SAPI === 'cli') {
fwrite(
STDOUT,
"Open in a browser (after: php -S 127.0.0.1:8080 -t docs):\n"
. $redirectUri . "\n\n"
. "Or paste this authorize URL:\n" . $url . "\n"
);
exit(0);
}

header('Location: ' . $url);
exit;
} catch (PenneoSdkRuntimeException $e) {
// something went wrong - handle the error
var_dump($e);
if (PHP_SAPI === 'cli') {
var_dump($e);
exit(1);
}
header('Content-Type: text/plain; charset=utf-8');
echo 'Could not build authorize URL: ' . $e->getMessage();
exit(1);
}
}

// the OAuth flow has finished, so we can start using the API
ApiConnector::initializeOAuth($penneoOAuth);

$casefile = new CaseFile();
$casefile->setTitle('new test casefile from PHP');
CaseFile::persist($casefile);

header('Content-Type: text/plain; charset=utf-8');
echo 'OK — Case file created. id=' . (string) $casefile->getId() . PHP_EOL;
2 changes: 2 additions & 0 deletions docs/programmatic_oauth_example.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
$casefile = new CaseFile();
$casefile->setTitle('new test casefile from PHP');
CaseFile::persist($casefile);

var_dump($casefile);
Loading
Loading