Skip to content

Conversation

@janepie
Copy link
Member

@janepie janepie commented Oct 15, 2025

Summary

Needed for Windmill integration.

This code adds an option to ask for authentication tokens when registering a webhook. The requested tokens will be added to the dispatched request to the defined endpoint.

All tokens generated with this have a lifetime of 1 hour and will be deleted after that in a background job every 5 min.

Tokens can be requested in the tokenNeeded parameter, which accepts listed values in the two fields "user_ids" and "user_roles".
"user_ids" is a list of user uids for which tokens are needed, "user_roles" is a 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. Possible roles: "owner" for the user creating the webhook, "trigger" for the user triggering the webhook call.

Checklist

@janepie janepie force-pushed the feat/webhook-tokens branch from e804cd1 to 5b188b6 Compare October 15, 2025 14:10
Copy link
Member

@julien-nc julien-nc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From you branch, at the top of server, you can run

composer i
# you might wanna revert what it does to lib/composer
git checkout ./lib/composer/
composer run cs:check ./apps/webhook_listeners/lib
# or if you have Php 8.4 like me and it's refused by php-cs
PHP_CS_FIXER_IGNORE_ENV=1 composer run cs:check ./apps/webhook_listeners/lib
# and if you want to fix the syntax issues:
composer run cs:fix ./apps/webhook_listeners/lib

(for later) To implement the token expiration mechanism you might need a new table where you store

  • the token ID
  • the webhook ID
  • if needed, the token creation timestamp

@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 33585c0 to 78ea408 Compare October 16, 2025 13:47
@janepie janepie changed the title Feat/webhook tokens Feat: add auth tokens to webhook call Oct 17, 2025
@janepie janepie force-pushed the feat/webhook-tokens branch from 78ea408 to 8dae570 Compare October 17, 2025 14:24
@janepie janepie force-pushed the feat/webhook-tokens branch from 8dae570 to 12747b1 Compare October 17, 2025 14:27
@janepie janepie changed the title Feat: add auth tokens to webhook call Feat(webhook_listeners): add auth tokens to webhook call Oct 17, 2025
@janepie janepie force-pushed the feat/webhook-tokens branch from 12747b1 to 8572f9a Compare October 17, 2025 14:47
@oleksandr-nc
Copy link
Contributor

Who will revoke generated token once it is not needed? Will we revoke it in the server repo or in the Windmill integration app itself?

Another question related to previous: If the token was generated for user bob first time, and then second time the flow was triggered - should we create a new token for user bob or update the olds token expiration time?

@julien-nc
Copy link
Member

@oleksandr-nc

Who will revoke generated token once it is not needed?

IMO the tokens should be revoked by a background job in webhook_listeners.

should we create a new token for user bob or update the olds token expiration time?

I think it's clearer and cleaner if we have independent tokens for each webhook "call". If the token generated for the first run leaks for some reason, it's bad if we make it live longer.

@oleksandr-nc
Copy link
Contributor

IMO the tokens should be revoked by a background job in webhook_listeners.

This definetly should be added as a comment to the code.

@julien-nc
Copy link
Member

Yep, this PR is still WIP. This is planned.

@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 0c0a98a to 7847d3b Compare October 30, 2025 16:57
@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 2d030cc to f53ad98 Compare November 12, 2025 11:31
@janepie janepie added this to the Nextcloud 33 milestone Nov 12, 2025
@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 647a122 to a6c77a7 Compare November 12, 2025 12:32
@janepie janepie requested a review from julien-nc November 12, 2025 12:35
@janepie janepie force-pushed the feat/webhook-tokens branch 5 times, most recently from 0ff523e to 954d099 Compare November 12, 2025 14:18
@janepie
Copy link
Member Author

janepie commented Nov 20, 2025

Sorry for all the change requests but this is quite a big PR.

No worries, thanks for reviewing so thoroughly! I think I got everything

$this->logger->debug('Invalidating ephemeral webhook tokens older than ' . date('c', $olderThan), ['app' => 'webhook_listeners']);
foreach ($tokensToDelete as $token) {
$this->tokenMapper->invalidate($this->tokenMapper->getTokenById($token->getTokenId())->getToken()); // delete token itself
$this->deleteByTokenId($token->getTokenId()); // delete db row in webhook_tokens
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling is missing here, I think. What if the token doesn't exist anymore in either table? The other tokens should still be deleted. Also, the getOlderThan could fail, I think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm if it's failing to delete in the authtokens table, should it stay in the second table or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOlderThan is a db query, what kind of failing are you thinking of?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query failure. All db queries can fail with a DB Exception. That should always be handled, IMO

@janepie janepie force-pushed the feat/webhook-tokens branch from 7f2528e to 0d081b6 Compare November 20, 2025 15:23
@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 3669f4c to c45c268 Compare November 20, 2025 15:50
$tokensToDelete = $this->getOlderThan($olderThan);
}
catch(Exception $e){
$this->logger->error('Webhook token deletion failed: ' . $e->getMessage(), ['exception' => $e]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return, please :)

@janepie janepie force-pushed the feat/webhook-tokens branch 2 times, most recently from 959d97d to be08ce8 Compare November 20, 2025 16:14
$client = $this->clientService->newClient();

// adding Ephemeral auth tokens to the call
$data['tokens'] = $this->tokenService->getTokens($webhookListener, $data['user']['uid']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$data['user'] may be null if an event is triggered from a public page.

Suggested change
$data['tokens'] = $this->tokenService->getTokens($webhookListener, $data['user']['uid']);
$data['tokens'] = $this->tokenService->getTokens($webhookListener, $data['user']['uid'] ?? null);

Something like that would work, if getTokens supports getting null as second parameter.

$this->logger->debug('Invalidating ephemeral webhook tokens older than ' . date('c', $olderThan), ['app' => 'webhook_listeners']);
foreach ($tokensToDelete as $token) {
try {
$this->tokenMapper->invalidate($this->tokenMapper->getTokenById($token->getTokenId())->getToken()); // delete token itself
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->tokenMapper->invalidate($this->tokenMapper->getTokenById($token->getTokenId())->getToken()); // delete token itself
$this->tokenMapper->delete($this->tokenMapper->getTokenById($token->getTokenId())); // delete token itself

];
break;
default:
$this->logger->error('Webhook token creation for user role ' . $function . ' not defined. ' . $e->getMessage(), ['exception' => $e]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no $e object in this context or did I miss something?

}
}
if (isset($tokenNeeded['user_roles'])) {
foreach ($tokenNeeded['user_roles'] as $function) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($tokenNeeded['user_roles'] as $function) {
foreach ($tokenNeeded['user_roles'] as $user_role) {

The namings $function and $functionId in this loop are quite confusing.

Comment on lines +109 to +114
// We need the getToken() method to be able to send the token out.
// That method is only available in PublicKeyToken which is returned by generateToken
// but not declared as such, so we have to check the type here
if (!($deviceToken instanceof PublicKeyToken)) { // type needed for the getToken() function
throw new \Exception('Unexpected token type');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// We need the getToken() method to be able to send the token out.
// That method is only available in PublicKeyToken which is returned by generateToken
// but not declared as such, so we have to check the type here
if (!($deviceToken instanceof PublicKeyToken)) { // type needed for the getToken() function
throw new \Exception('Unexpected token type');
}

This is not true anymore, only getId is used and it’s part of the public interface.

Signed-off-by: Jana Peper <[email protected]>
@janepie janepie force-pushed the feat/webhook-tokens branch from be08ce8 to a38ab95 Compare November 24, 2025 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants