Skip to content

feat(provisioning-api): Add endpoint to invalidate user tokens #52253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

printminion-co
Copy link
Contributor

@printminion-co printminion-co commented Apr 17, 2025

Summary

Introduce a core API endpoint for external user management systems to invalidate user tokens.
This action removes all tokens of the target user, effectively logging them out from all sessions (browser, apps) without wiping data on their devices.

# usage example
curl -s -X DELETE "$NEXTCLOUD_URL/ocs/v2.php/cloud/users/${testUserId}/sessions?format=json" \
	-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
	-H "OCS-APIRequest: true" \
	-H "User-Agent: $USER_AGENT"

Test

  • create user
  • add 3 clients to the user
  • log with user via new incognito browser session to NC
  • check current token count
  • make invalidate API call (as admin user)
  • Observe
    • User is logged out in incognito browser window
    • User has no app tokens in the list
    • Observe user is logged out from all devices.
    • Observe data on devices was not wiped."

You can use following script

./test_token_invalidation_api.sh
test_token_invalidation_api.sh
#!/usr/bin/env bash

# This script tests the creation, authentication, and invalidation of user tokens
# in a Nextcloud instance via its OCS API. It performs the following steps:
# 1. Verifies admin credentials and API access.
# 2. Checks if a test user exists, creates the user if not present.
# 3. Generates three authentication tokens for the test user.
# 4. Prompts the user to manually verify tokens in the browser.
# 5. Invalidates all authentication tokens for the test user via the API.
# 6. Prompts the user to verify that the test user is logged out from all devices.


ADMIN_USERNAME="admin"
ADMIN_PASSWORD="admin"
USER_AGENT="HiDrive Next Test Client"

NEXTCLOUD_URL="http://localhost:8080"

# username to be tested on
testUserId="foo"
testUserPass="foo"

echo "[i] Testing user 'admin' credentials on API..."
response=$(curl -s -X GET "$NEXTCLOUD_URL/ocs/v2.php/cloud/user?format=json" \
	-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
	-H "OCS-APIRequest: true" \
	-H "User-Agent: $USER_AGENT")

if echo "$response" | grep -q '"status":"ok"'; then
	echo "[i] User '$ADMIN_USERNAME' is logged on successfully."
else
	echo "[w] User '$ADMIN_USERNAME' does not exist or API request failed."
	echo "[w] Response: "
	echo "$response" | jq
	exit 1
fi

echo "[i] Testing user '${testUserId}' existence..."
isUserExists=false
response=$(curl -s -X GET "$NEXTCLOUD_URL/ocs/v1.php/cloud/users/${testUserId}?format=json" \
	-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
	-H "OCS-APIRequest: true" \
	-H "User-Agent: $USER_AGENT")

if echo "$response" | grep -q '"status":"ok"'; then
	echo "[i] User '${testUserId}' exists."
	isUserExists=true
else
	echo "[i] User '${testUserId}' does not exist."
fi

if [ "$isUserExists" = false ]; then
	echo "[i] Creating user '${testUserId}'..."
	response=$(curl -s -X POST "$NEXTCLOUD_URL/ocs/v1.php/cloud/users?format=json" \
		-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
		-H "OCS-APIRequest: true" \
		-H "User-Agent: $USER_AGENT" \
		-d "userid=${testUserId}&password=${testUserPass}")

	if echo "$response" | grep -q '"status":"ok"'; then
		echo "[i] User '${testUserId}' created successfully."
	else
		echo "[e] Failed to create user '${testUserId}'."
		echo "[e] Response: "
		echo "$response" | jq
	fi
fi

echo "[i] Creating user '${testUserId}' 3 auth-tokens..."
for i in {1..3}; do
	response=$(curl -s "$NEXTCLOUD_URL/ocs/v2.php/core/getapppassword?format=json" \
		-u "${testUserId}:${testUserPass}" \
		-H "OCS-APIRequest: true" \
		-H "User-Agent: $USER_AGENT")

	if echo "$response" | grep -q '"status":"ok"'; then
		echo "[i] Token ${i} created successfully."
	else
		echo "[e] Failed to create token ${i}."
		echo "[e] Response: "
		echo "$response" | jq
	fi
done

echo "[!] Open browser at ${NEXTCLOUD_URL} and login as '${testUserId}' with password '${testUserPass}'..."
echo "[!] Check tokens $NEXTCLOUD_URL/index.php/settings/user/security"
read -r -p "[?] Press any key to continue... " -n1 -s
echo

echo "[i] Invalidating user '${testUserId}' auth-tokens via API call..."
response=$(curl -s -X DELETE "$NEXTCLOUD_URL/ocs/v2.php/cloud/users/${testUserId}/sessions?format=json" \
	-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
	-H "OCS-APIRequest: true" \
	-H "User-Agent: $USER_AGENT")

if echo "$response" | grep -q '"status":"ok"'; then
	echo "[i] User '${testUserId}' token invalidation is successful."
	echo "[i] Response: "
	echo "$response" | jq
else
	echo "[e] Failed to invalidate user '${testUserId}' tokens."
	echo "[e] Response: "
	echo "$response" | jq
fi

echo "[!] Reload browser and check if you are logged out."
echo "[!] User has no app tokens in the list"
echo "[!] Observe user is logged out from all devices."
echo "[!] Observe data on devices was *not* wiped."

Unitests

phpunit --configuration tests/phpunit-autotest-user-invalidate.xml
tests/phpunit-autotest-user-invalidate.xml ```xml ./lib/User/SessionTest.php ./lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php ./Core/Controller/ClientFlowLoginV2ControllerTest.php ./Core/Service/LoginFlowV2ServiceUnitTest.php ./Core/Controller/AppPasswordControllerTest.php ./Core/Controller/ClientFlowLoginControllerTest.php ./Core/Controller/WipeControllerTest.php ./Core/Controller/UserControllerTest.php ./lib/Authentication/Token/RemoteWipeTest.php ./lib/Authentication/Token/InvalidatorTest.php ../core/* ../lib/private/* ../**/ ../3rdparty/**/* ../apps/**/* ../apps-custom/**/* ../apps-external/** ../apps-custom/** ../build ../IONOS/**/* ../lib/composer ../vendor/**/* ../tests ```
phpunit --configuration tests/phpunit-autotest-external-provisioning_api.xml
tests/phpunit-autotest-user-invalidate.xml ```xml ../apps/provisioning_api/tests ../lib/private/Files/Storage/DAV.php ../apps/provisioning_api ../apps/provisioning_api/l10n ../apps/provisioning_api/3rdparty ../apps/provisioning_api/tests ```

Checklist

@printminion-co printminion-co requested review from provokateurin and a team as code owners April 17, 2025 12:34
@printminion-co printminion-co requested review from yemkareems and come-nc and removed request for a team April 17, 2025 12:34
Comment on lines +1283 to +1285
#[NoAdminRequired]
public function invalidateUserTokens(string $userId): DataResponse {
Copy link
Member

Choose a reason for hiding this comment

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

Big no no if anyone can do this to any user!

@provokateurin
Copy link
Member

Ok I see you still have a user check in place, but from the description it really sounds like you only want admins to do this.
That will not work with this PR, because you only allow every user to delete their own tokens and not the admin user as well.

Comment on lines 29 to 42
public function invalidateAllUserTokens(string $uid): bool {
$this->logger->info("Invalidating all tokens for user: $uid");
$this->eventDispatcher->dispatch(TokensInvalidationStarted::class, new TokensInvalidationStarted($uid));

$tokens = $this->tokenProvider->getTokenByUser($uid);
foreach ($tokens as $token) {
$this->tokenProvider->invalidateTokenById($uid, $token->getId());
}

$this->eventDispatcher->dispatch(TokensInvalidationFinished::class, new TokensInvalidationFinished($uid));
return true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not add that method to PublicKeyTokenProvider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PublicKeyTokenProvider has no imported IEventDispatcher.
We would like to be able to log events via the admin_audit app

Copy link
Contributor

@artonge artonge May 6, 2025

Choose a reason for hiding this comment

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

Feel free to add it to the constructor arguments.

Comment on lines +1294 to +1297
if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
throw new OCSException('', 101);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inspired by \OCA\Provisioning_API\Controller\UsersController::wipeUserDevices

throw new OCSException('', 101);

Comment on lines +1299 to +1305
$subAdminManager = $this->groupManager->getSubAdmin();
$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we have the same kind of code in other places, but it’s inefficient, because it will try to see if the user is a subadmin even if he’s an admin, which may be expensive if groups are through a backend with subgroups and stuff.
So, please try to write this in a way where it only computes needed information.

Copy link
Contributor

Choose a reason for hiding this comment

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

In the context of this PR, I won't mind the copy past. If we want a better way of doing it, let's do it in another PR.

Comment on lines +12 to +13
use OC\Authentication\Events\TokensInvalidationFinished;
use OC\Authentication\Events\TokensInvalidationStarted;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these events listened to by anyone?
If the intent is that applications may listen to them, they should be part of OCP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea was to consume it via admin_audit app

Copy link
Contributor

Choose a reason for hiding this comment

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

Then it needs to be part of public API in OCP.

Copy link
Contributor

Choose a reason for hiding this comment

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

@printminion-co please move those events to the lib/public/Authentication/Events directory. And set their namespace to OCP\Authentication\Events;

@printminion-co printminion-co force-pushed the feat/add_rest_for_user_token_invalidation branch 2 times, most recently from 79a9700 to 826559d Compare April 22, 2025 12:50
@printminion-co printminion-co changed the title feat(provisioning-api): Add user tokens invalidation to core api feat(provisioning-api): Add endpoint to invalidate user tokens Apr 22, 2025
@printminion-co printminion-co force-pushed the feat/add_rest_for_user_token_invalidation branch 2 times, most recently from 0e9fab4 to 69d095a Compare April 22, 2025 15:47
@printminion-co printminion-co requested a review from artonge April 25, 2025 10:31
Copy link
Contributor

github-actions bot commented May 2, 2025

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@printminion-co printminion-co force-pushed the feat/add_rest_for_user_token_invalidation branch from 69d095a to 51de786 Compare May 6, 2025 07:33
@artonge artonge requested a review from come-nc May 6, 2025 09:28
Introduce a core API endpoint for external user management systems to invalidate user tokens.
This action removes all tokens of the target user, effectively logging them out from all sessions (browser, apps) without wiping data on their devices.

### Usage example:
```bash
curl -s -X DELETE "$NEXTCLOUD_URL/ocs/v2.php/cloud/users/${testUserId}/sessions?format=json" \
	-u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
	-H "OCS-APIRequest: true"
```

Signed-off-by: Misha M.-Kupriyanov <kupriyanov@strato.de>
@printminion-co printminion-co force-pushed the feat/add_rest_for_user_token_invalidation branch from 51de786 to 19015d8 Compare May 6, 2025 12:02
@printminion-co
Copy link
Contributor Author

Our team has decided to deprioritize the approach outlined in that pull request.

If Nextcloud is interested in pursuing this feature, please feel free to maintain the PR. Otherwise, I will proceed to close it in the near future.

@printminion-co printminion-co marked this pull request as draft May 7, 2025 14:38
@artonge artonge closed this May 12, 2025
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.

4 participants