Skip to content

Commit f2bc07f

Browse files
andris9claude
andcommitted
feat: refactor getAuthenticationUrl to use API and add webhook signature verification
- Change getAuthenticationUrl() to call /v1/authentication/form API endpoint instead of generating URLs client-side, enabling server-side nonce and timestamp for replay attack protection - Add support for new authentication parameters: type, delegated, syncFrom, notifyFrom, subconnections, and path - Add verifyWebhookSignature() method to validate webhook requests using HMAC-SHA256 with timing-safe comparison - Update README with new parameters table and webhook verification docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8ab9cfa commit f2bc07f

3 files changed

Lines changed: 255 additions & 38 deletions

File tree

README.md

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ $result = $client->messages->submit('account-id', [
4949
$client = new EmailEngine(
5050
accessToken: 'your-access-token', // Required: API access token
5151
baseUrl: 'http://localhost:3000', // EmailEngine base URL (default: localhost:3000)
52-
serviceSecret: 'your-service-secret', // For hosted authentication URLs
53-
redirectUrl: 'http://your-app/callback', // Default redirect URL for auth
52+
serviceSecret: 'your-service-secret', // For verifying webhook signatures
53+
redirectUrl: 'http://your-app/callback', // Default redirect URL for hosted auth
5454
timeout: 30, // Request timeout in seconds
5555
);
5656
```
@@ -266,17 +266,16 @@ $client->settings->setWebhooks([
266266

267267
### Hosted Authentication
268268

269-
Generate URLs for EmailEngine's hosted authentication form:
269+
Generate URLs for EmailEngine's hosted authentication form. This method calls the EmailEngine API to generate a secure authentication URL with server-side nonce and timestamp for replay attack protection.
270270

271271
```php
272272
$client = new EmailEngine(
273273
accessToken: 'your-token',
274274
baseUrl: 'http://localhost:3000',
275-
serviceSecret: 'your-service-secret',
276275
redirectUrl: 'http://your-app/auth-callback',
277276
);
278277

279-
// Generate auth URL
278+
// Generate auth URL with basic options
280279
$authUrl = $client->getAuthenticationUrl([
281280
'account' => null, // null = auto-generate account ID
282281
'name' => 'User Name',
@@ -287,6 +286,62 @@ $authUrl = $client->getAuthenticationUrl([
287286
header('Location: ' . $authUrl);
288287
```
289288

289+
#### Available Parameters
290+
291+
| Parameter | Type | Description |
292+
|-----------|------|-------------|
293+
| `account` | string\|null | Account ID (null to auto-generate) |
294+
| `name` | string | Display name for the account |
295+
| `email` | string | Email address hint |
296+
| `redirectUrl` | string | Override default redirect URL |
297+
| `type` | string | Account type (e.g., 'imap', 'gmail', 'outlook') |
298+
| `delegated` | bool | Enable delegated access |
299+
| `syncFrom` | string | ISO 8601 date to sync messages from |
300+
| `notifyFrom` | string | ISO 8601 date to send notifications from |
301+
| `subconnections` | array | List of shared mailboxes to connect |
302+
| `path` | string\|array | Mailbox path(s) to monitor |
303+
304+
```php
305+
// Example with all parameters
306+
$authUrl = $client->getAuthenticationUrl([
307+
'account' => 'user-123',
308+
'name' => 'John Doe',
309+
'email' => 'john@example.com',
310+
'type' => 'gmail',
311+
'delegated' => true,
312+
'syncFrom' => '2024-01-01T00:00:00Z',
313+
'notifyFrom' => '2024-01-01T00:00:00Z',
314+
'subconnections' => ['Shared Mailbox'],
315+
'path' => ['INBOX', 'Sent'],
316+
]);
317+
```
318+
319+
### Webhook Signature Verification
320+
321+
Verify webhook signatures to ensure requests are authentically from EmailEngine. Requires the `serviceSecret` to be configured.
322+
323+
```php
324+
$client = new EmailEngine(
325+
accessToken: 'your-token',
326+
baseUrl: 'http://localhost:3000',
327+
serviceSecret: 'your-service-secret',
328+
);
329+
330+
// Get the raw request body and signature header
331+
$body = file_get_contents('php://input');
332+
$signature = $_SERVER['HTTP_X_EE_WH_SIGNATURE'] ?? '';
333+
334+
if ($client->verifyWebhookSignature($body, $signature)) {
335+
// Signature is valid - process the webhook
336+
$payload = json_decode($body, true);
337+
// ... handle webhook event
338+
} else {
339+
// Invalid signature - reject the request
340+
http_response_code(401);
341+
exit('Invalid signature');
342+
}
343+
```
344+
290345
### Outbox Management
291346

292347
```php

src/EmailEngine.php

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,34 +218,67 @@ public function blocklists(): Blocklists
218218
/**
219219
* Generate a redirect URL for EmailEngine's hosted authentication page
220220
*
221+
* This method calls the EmailEngine API to generate a secure authentication URL
222+
* with server-side nonce and timestamp for replay attack protection.
223+
*
221224
* @param array{
222225
* account?: string|null,
223226
* name?: string,
224227
* email?: string,
225-
* redirectUrl?: string
228+
* redirectUrl?: string,
229+
* type?: string,
230+
* delegated?: bool,
231+
* syncFrom?: string,
232+
* notifyFrom?: string,
233+
* subconnections?: array<string>,
234+
* path?: string|array<string>
226235
* } $data Authentication data
227-
* @throws EmailEngineException If service secret is not configured
236+
* @throws EmailEngineException If the API request fails
228237
*/
229238
public function getAuthenticationUrl(array $data = []): string
230239
{
231-
if ($this->serviceSecret === null) {
240+
$data['redirectUrl'] ??= $this->redirectUrl;
241+
242+
if (empty($data['redirectUrl'])) {
243+
throw new EmailEngineException(
244+
message: 'redirectUrl is required for generating authentication URLs'
245+
);
246+
}
247+
248+
$response = $this->httpClient->post('/v1/authentication/form', $data);
249+
250+
if (!isset($response['url'])) {
232251
throw new EmailEngineException(
233-
message: 'Service secret is required for generating authentication URLs'
252+
message: 'Invalid response from authentication form API: missing url field'
234253
);
235254
}
236255

237-
// Use provided redirectUrl or fall back to default
238-
if (empty($data['redirectUrl']) && $this->redirectUrl !== null) {
239-
$data['redirectUrl'] = $this->redirectUrl;
256+
return $response['url'];
257+
}
258+
259+
/**
260+
* Verify a webhook signature from EmailEngine
261+
*
262+
* EmailEngine signs webhooks with the X-EE-Wh-Signature header using
263+
* HMAC-SHA256 of the raw request body, base64url encoded.
264+
*
265+
* @param string $body The raw webhook request body
266+
* @param string $signature The signature from X-EE-Wh-Signature header
267+
* @return bool True if the signature is valid
268+
* @throws EmailEngineException If service secret is not configured
269+
*/
270+
public function verifyWebhookSignature(string $body, string $signature): bool
271+
{
272+
if ($this->serviceSecret === null) {
273+
throw new EmailEngineException(
274+
message: 'Service secret is required for verifying webhook signatures'
275+
);
240276
}
241277

242-
$dataJson = json_encode($data, JSON_THROW_ON_ERROR);
243-
$signature = $this->signRequest($dataJson, $this->serviceSecret);
278+
$computed = hash_hmac('sha256', $body, $this->serviceSecret, true);
279+
$computedBase64 = $this->base64EncodeUrlsafe($computed);
244280

245-
return $this->baseUrl . '/accounts/new?data=' .
246-
$this->base64EncodeUrlsafe($dataJson) .
247-
'&sig=' .
248-
$this->base64EncodeUrlsafe($signature);
281+
return hash_equals($computedBase64, $signature);
249282
}
250283

251284
/**

0 commit comments

Comments
 (0)