Skip to content

Commit 4dffa1a

Browse files
Merge pull request #55790 from nextcloud/feat/webhook-tokens
Feat(webhook_listeners): add auth tokens to webhook call
2 parents 38792c8 + 694ecce commit 4dffa1a

File tree

15 files changed

+561
-1
lines changed

15 files changed

+561
-1
lines changed

apps/webhook_listeners/appinfo/info.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Administrators can configure webhook listeners via the app's OCS API. The app al
1818
]]>
1919
</description>
2020

21-
<version>1.4.1</version>
21+
<version>1.5.0</version>
2222
<licence>agpl</licence>
2323
<author>Côme Chilliet</author>
2424
<namespace>WebhookListeners</namespace>
@@ -49,4 +49,8 @@ Administrators can configure webhook listeners via the app's OCS API. The app al
4949
<admin-delegation>OCA\WebhookListeners\Settings\Admin</admin-delegation>
5050
<admin-delegation-section>OCA\WebhookListeners\Settings\AdminSection</admin-delegation-section>
5151
</settings>
52+
53+
<background-jobs>
54+
<job>OCA\WebhookListeners\BackgroundJobs\WebhookTokenCleanup</job>
55+
</background-jobs>
5256
</info>

apps/webhook_listeners/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@
99
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
1010
'OCA\\WebhookListeners\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1111
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php',
12+
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => $baseDir . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
1213
'OCA\\WebhookListeners\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php',
1314
'OCA\\WebhookListeners\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php',
1415
'OCA\\WebhookListeners\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php',
16+
'OCA\\WebhookListeners\\Db\\EphemeralToken' => $baseDir . '/../lib/Db/EphemeralToken.php',
17+
'OCA\\WebhookListeners\\Db\\EphemeralTokenMapper' => $baseDir . '/../lib/Db/EphemeralTokenMapper.php',
1518
'OCA\\WebhookListeners\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php',
1619
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php',
1720
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php',
1821
'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php',
1922
'OCA\\WebhookListeners\\Migration\\Version1001Date20240716184935' => $baseDir . '/../lib/Migration/Version1001Date20240716184935.php',
23+
'OCA\\WebhookListeners\\Migration\\Version1500Date20251007130000' => $baseDir . '/../lib/Migration/Version1500Date20251007130000.php',
2024
'OCA\\WebhookListeners\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
2125
'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => $baseDir . '/../lib/Service/PHPMongoQuery.php',
26+
'OCA\\WebhookListeners\\Service\\TokenService' => $baseDir . '/../lib/Service/TokenService.php',
2227
'OCA\\WebhookListeners\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
2328
'OCA\\WebhookListeners\\Settings\\AdminSection' => $baseDir . '/../lib/Settings/AdminSection.php',
2429
);

apps/webhook_listeners/composer/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,21 @@ class ComposerStaticInitWebhookListeners
2424
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
2525
'OCA\\WebhookListeners\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2626
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php',
27+
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
2728
'OCA\\WebhookListeners\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php',
2829
'OCA\\WebhookListeners\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php',
2930
'OCA\\WebhookListeners\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php',
31+
'OCA\\WebhookListeners\\Db\\EphemeralToken' => __DIR__ . '/..' . '/../lib/Db/EphemeralToken.php',
32+
'OCA\\WebhookListeners\\Db\\EphemeralTokenMapper' => __DIR__ . '/..' . '/../lib/Db/EphemeralTokenMapper.php',
3033
'OCA\\WebhookListeners\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php',
3134
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php',
3235
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php',
3336
'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php',
3437
'OCA\\WebhookListeners\\Migration\\Version1001Date20240716184935' => __DIR__ . '/..' . '/../lib/Migration/Version1001Date20240716184935.php',
38+
'OCA\\WebhookListeners\\Migration\\Version1500Date20251007130000' => __DIR__ . '/..' . '/../lib/Migration/Version1500Date20251007130000.php',
3539
'OCA\\WebhookListeners\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
3640
'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => __DIR__ . '/..' . '/../lib/Service/PHPMongoQuery.php',
41+
'OCA\\WebhookListeners\\Service\\TokenService' => __DIR__ . '/..' . '/../lib/Service/TokenService.php',
3742
'OCA\\WebhookListeners\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
3843
'OCA\\WebhookListeners\\Settings\\AdminSection' => __DIR__ . '/..' . '/../lib/Settings/AdminSection.php',
3944
);

apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\AppAPI\PublicFunctions;
1313
use OCA\WebhookListeners\Db\AuthMethod;
1414
use OCA\WebhookListeners\Db\WebhookListenerMapper;
15+
use OCA\WebhookListeners\Service\TokenService;
1516
use OCP\App\IAppManager;
1617
use OCP\AppFramework\Utility\ITimeFactory;
1718
use OCP\BackgroundJob\QueuedJob;
@@ -30,6 +31,7 @@ public function __construct(
3031
private WebhookListenerMapper $mapper,
3132
private LoggerInterface $logger,
3233
private IAppManager $appManager,
34+
private TokenService $tokenService,
3335
ITimeFactory $timeFactory,
3436
) {
3537
parent::__construct($timeFactory);
@@ -42,6 +44,9 @@ protected function run($argument): void {
4244
[$data, $webhookId] = $argument;
4345
$webhookListener = $this->mapper->getById($webhookId);
4446
$client = $this->clientService->newClient();
47+
48+
// adding Ephemeral auth tokens to the call
49+
$data['tokens'] = $this->tokenService->getTokens($webhookListener, $data['user']['uid'] ?? null);
4550
$options = [
4651
'verify' => $this->certificateManager->getAbsoluteBundlePath(),
4752
'headers' => $webhookListener->getHeaders() ?? [],
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\BackgroundJobs;
11+
12+
use OCA\WebhookListeners\Db\EphemeralTokenMapper;
13+
use OCP\AppFramework\Utility\ITimeFactory;
14+
use OCP\BackgroundJob\TimedJob;
15+
16+
class WebhookTokenCleanup extends TimedJob {
17+
18+
public function __construct(
19+
private EphemeralTokenMapper $tokenMapper,
20+
ITimeFactory $timeFactory,
21+
) {
22+
parent::__construct($timeFactory);
23+
// every 5 min
24+
$this->setInterval(5 * 60);
25+
}
26+
27+
/**
28+
* @param array $argument
29+
*/
30+
protected function run($argument): void {
31+
$this->tokenMapper->invalidateOldTokens();
32+
}
33+
}

apps/webhook_listeners/lib/Controller/WebhooksController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public function show(int $id): DataResponse {
112112
* @param ?array<string,string> $headers Array of headers to send
113113
* @param "none"|"header"|null $authMethod Authentication method to use
114114
* @param ?array<string,mixed> $authData Array of data for authentication
115+
* @param ?array{user_ids?:list<string>,user_roles?:list<string>} $tokenNeeded
116+
* List of user ids for which to include auth tokens in the event.
117+
* Has two fields: "user_ids" list of user uids for which tokens are needed, "user_roles" list of roles (users not defined by their ID but by the role they have in the webhook event) for which tokens can be included.
118+
* Possible roles: "owner" for the user creating the webhook, "trigger" for the user triggering the webhook call.
119+
* Requested auth tokens are valid for 1 hour after receiving them in the event call request.
115120
*
116121
* @return DataResponse<Http::STATUS_OK, WebhookListenersWebhookInfo, array{}>
117122
*
@@ -134,6 +139,7 @@ public function create(
134139
?string $authMethod,
135140
#[\SensitiveParameter]
136141
?array $authData,
142+
?array $tokenNeeded = null,
137143
): DataResponse {
138144
$appId = null;
139145
if ($this->session->get('app_api') === true) {
@@ -156,6 +162,7 @@ public function create(
156162
$headers,
157163
$authMethod,
158164
$authData,
165+
$tokenNeeded,
159166
);
160167
return new DataResponse($webhookListener->jsonSerialize());
161168
} catch (\UnexpectedValueException $e) {
@@ -180,6 +187,11 @@ public function create(
180187
* @param ?array<string,string> $headers Array of headers to send
181188
* @param "none"|"header"|null $authMethod Authentication method to use
182189
* @param ?array<string,mixed> $authData Array of data for authentication
190+
* @param ?array{user_ids?:list<string>,user_roles?:list<string>} $tokenNeeded
191+
* List of user ids for which to include auth tokens in the event.
192+
* Has two fields: "user_ids" list of user uids for which tokens are needed, "user_roles" list of roles (users not defined by their ID but by the role they have in the webhook event) for which tokens can be included.
193+
* Possible roles: "owner" for the user creating the webhook, "trigger" for the user triggering the webhook call.
194+
* Requested auth tokens are valid for 1 hour after receiving them in the event call request.
183195
*
184196
* @return DataResponse<Http::STATUS_OK, WebhookListenersWebhookInfo, array{}>
185197
*
@@ -203,6 +215,7 @@ public function update(
203215
?string $authMethod,
204216
#[\SensitiveParameter]
205217
?array $authData,
218+
?array $tokenNeeded = null,
206219
): DataResponse {
207220
$appId = null;
208221
if ($this->session->get('app_api') === true) {
@@ -226,6 +239,7 @@ public function update(
226239
$headers,
227240
$authMethod,
228241
$authData,
242+
$tokenNeeded,
229243
);
230244
return new DataResponse($webhookListener->jsonSerialize());
231245
} catch (\UnexpectedValueException $e) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\Db;
11+
12+
use OCP\AppFramework\Db\Entity;
13+
14+
/**
15+
* @method int getTokenId()
16+
* @method ?string getUserId()
17+
* @method int getCreatedAt()
18+
* @psalm-suppress PropertyNotSetInConstructor
19+
*/
20+
class EphemeralToken extends Entity implements \JsonSerializable {
21+
/**
22+
* @var int id of the token in the oc_authtoken db table
23+
*/
24+
protected $tokenId;
25+
26+
/**
27+
* @var ?string id of the user wich the token belongs to
28+
* @psalm-suppress PropertyNotSetInConstructor
29+
*/
30+
protected $userId = null;
31+
32+
/**
33+
* @var int token creation timestamp
34+
* @psalm-suppress PropertyNotSetInConstructor
35+
*/
36+
protected $createdAt;
37+
38+
public function __construct() {
39+
$this->addType('tokenId', 'integer');
40+
$this->addType('userId', 'string');
41+
$this->addType('createdAt', 'integer');
42+
}
43+
44+
public function jsonSerialize(): array {
45+
$fields = array_keys($this->getFieldTypes());
46+
return array_combine(
47+
$fields,
48+
array_map(
49+
fn ($field) => $this->getter($field),
50+
$fields
51+
)
52+
);
53+
}
54+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\Db;
11+
12+
use OC\Authentication\Token\PublicKeyTokenMapper;
13+
use OCP\AppFramework\Db\DoesNotExistException;
14+
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
15+
use OCP\AppFramework\Db\QBMapper;
16+
use OCP\AppFramework\Utility\ITimeFactory;
17+
use OCP\DB\Exception;
18+
use OCP\DB\QueryBuilder\IQueryBuilder;
19+
use OCP\IDBConnection;
20+
use Psr\Log\LoggerInterface;
21+
22+
/**
23+
* @template-extends QBMapper<EphemeralToken>
24+
*/
25+
26+
class EphemeralTokenMapper extends QBMapper {
27+
public const TABLE_NAME = 'webhook_tokens';
28+
public const TOKEN_LIFETIME = 1 * 1 * 60; // one hour in seconds
29+
30+
public function __construct(
31+
IDBConnection $db,
32+
private LoggerInterface $logger,
33+
private ITimeFactory $time,
34+
private PublicKeyTokenMapper $tokenMapper,
35+
) {
36+
parent::__construct($db, self::TABLE_NAME, EphemeralToken::class);
37+
}
38+
39+
/**
40+
* @throws DoesNotExistException
41+
* @throws MultipleObjectsReturnedException
42+
* @throws Exception
43+
*/
44+
public function getById(int $id): EphemeralToken {
45+
$qb = $this->db->getQueryBuilder();
46+
47+
$qb->select('*')
48+
->from($this->getTableName())
49+
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
50+
51+
return $this->findEntity($qb);
52+
}
53+
54+
/**
55+
* @throws Exception
56+
* @return EphemeralToken[]
57+
*/
58+
public function getAll(): array {
59+
$qb = $this->db->getQueryBuilder();
60+
61+
$qb->select('*')
62+
->from($this->getTableName());
63+
64+
return $this->findEntities($qb);
65+
}
66+
67+
68+
/**
69+
* @param int $olderThan
70+
* @return EphemeralToken[]
71+
* @throws Exception
72+
*/
73+
public function getOlderThan($olderThan): array {
74+
$qb = $this->db->getQueryBuilder();
75+
76+
$qb->select('*')
77+
->from($this->getTableName())
78+
->where($qb->expr()->lt('created_at', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)));
79+
80+
return $this->findEntities($qb);
81+
}
82+
83+
/**
84+
* @throws Exception
85+
*/
86+
public function addEphemeralToken(
87+
int $tokenId,
88+
?string $userId,
89+
int $createdAt,
90+
): EphemeralToken {
91+
$tempToken = EphemeralToken::fromParams(
92+
[
93+
'tokenId' => $tokenId,
94+
'userId' => $userId,
95+
'createdAt' => $createdAt,
96+
]
97+
);
98+
return $this->insert($tempToken);
99+
}
100+
public function invalidateOldTokens(int $token_lifetime = self::TOKEN_LIFETIME) {
101+
$olderThan = $this->time->getTime() - $token_lifetime;
102+
try {
103+
$tokensToDelete = $this->getOlderThan($olderThan);
104+
} catch (Exception $e) {
105+
$this->logger->error('Webhook token deletion failed: ' . $e->getMessage(), ['exception' => $e]);
106+
return;
107+
}
108+
109+
110+
$this->logger->debug('Invalidating ephemeral webhook tokens older than ' . date('c', $olderThan), ['app' => 'webhook_listeners']);
111+
foreach ($tokensToDelete as $token) {
112+
try {
113+
$this->tokenMapper->delete($this->tokenMapper->getTokenById($token->getTokenId())); // delete token itself
114+
$this->delete($token); // delete db row in webhook_tokens
115+
} catch (Exception $e) {
116+
$this->logger->error('Webhook token deletion failed: ' . $e->getMessage(), ['exception' => $e]);
117+
}
118+
119+
}
120+
}
121+
}

apps/webhook_listeners/lib/Db/WebhookListener.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* @method ?string getAuthData()
2424
* @method void setAuthData(?string $data)
2525
* @method string getAuthMethod()
26+
* @method ?array getTokenNeeded()
2627
* @psalm-suppress PropertyNotSetInConstructor
2728
*/
2829
class WebhookListener extends Entity implements \JsonSerializable {
@@ -84,8 +85,15 @@ class WebhookListener extends Entity implements \JsonSerializable {
8485
*/
8586
protected $authData = null;
8687

88+
/**
89+
* @var array
90+
* @psalm-suppress PropertyNotSetInConstructor
91+
*/
92+
protected $tokenNeeded;
93+
8794
private ICrypto $crypto;
8895

96+
8997
public function __construct(
9098
?ICrypto $crypto = null,
9199
) {
@@ -103,6 +111,7 @@ public function __construct(
103111
$this->addType('headers', 'json');
104112
$this->addType('authMethod', 'string');
105113
$this->addType('authData', 'string');
114+
$this->addType('tokenNeeded', 'json');
106115
}
107116

108117
public function getAuthMethodEnum(): AuthMethod {

0 commit comments

Comments
 (0)