one-time passwords (e.g. for public file shares) - #61722
Conversation
adbd854 to
ae1bd6a
Compare
ae1bd6a to
5035daf
Compare
5035daf to
99f7621
Compare
edb4db5 to
27730a7
Compare
bc04d3a to
9a9f4d3
Compare
|
From my side, this PR is ready for review. :) The only thing missing, is documentation, but I would want to wait for feedback before working on that. Let me know if there is anything you require from me. Here are curl commands to create an OTP protected share on a Nextcloud instance running at http://nextcloud.local with user=admin and password=admin (requires the otp_provider_debug and otp_provider_email apps to be enabled): For email: curl http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares -u 'admin:admin' -H 'Content-Type: application/json' -H "Accept: application/json" -H "OCS-APIRequest: true" --data '{"path": "/Nextcloud_Server_Administration_Manual.pdf", "attributes": "[]", "shareType": 3, "otpProvider": "email", "otpRecipient": "your@email.address"}'For debug (OTPs will be written to Nextcloud logs, requires https://github.com/theCalcaholic/otp_provider_debug): curl http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares -u 'admin:admin' -H 'Content-Type: application/json' -H "Accept: application/json" -H "OCS-APIRequest: true" --data '{"path": "/Nextcloud_Server_Administration_Manual.pdf", "attributes": "[]", "shareType": 3, "otpProvider": "debug", "otpRecipient": "foobar"}'If the file |
f79b8ba to
702cee9
Compare
|
@susnux I hope it's ok to tag you (I wasn't sure if the update messages above would reach you otherwise). |
|
Other than that:
|
|
@miaulalala I have now checked all failed jobs and don't think they are related to issues in this PR, apart from the translation issue (see my notes above). |
|
Hello there, 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.) |
333c9d6 to
86707c0
Compare
|
Just did another rebase on master, since the upstream PR is merged now. :) |
c9a35eb to
1c2d696
Compare
|
Previously, I seem to have made a mistake while rebasing, resulting in a duplicate function and breaking the playwright tests. This should be fixed now |
|
Nice, now CI finally looks as expected. |
miaulalala
left a comment
There was a problem hiding this comment.
Security-related
-
Uncaught
OTPProviderNotFoundExceptioninShareOTPController::request()— the catch block only handlesOTPSendException, notOTPProviderNotFoundException. If a share's OTP provider gets disabled/uninstalled between share creation and an OTP request, this will 500 instead of failing gracefully.apps/files_sharing/lib/Controller/ShareOTPController.php -
requestotpis a state-changingGETroute with no CSRF protection. It sends a real email and mutates the OTP row, but as aGETit can be triggered by link prefetching, crawlers, or an<img src>— no user interaction required. RecommendPOST+ explicit CSRF handling.apps/files_sharing/appinfo/routes.php -
IManager::createOTP()accepts a raw plaintext$passwordwith no hashing guarantee at the public interface boundary — only the internalsendOTP()codepath hashes before persisting. Any third-party app (or future core code) callingcreateOTP()directly could store OTPs in plaintext. Suggest either dropping that parameter from the public API or hashing centrally insidecreateOTP().lib/public/OneTimePassword/IManager.php -
Unescaped HTML interpolation in the OTP email body —
SendOneTimePasswordEventListener::sendEmail()concatenates$recipientMsgdirectly into HTML. No attacker-controlled input reaches it today, but it's fragile: any future caller passing user-controlled text would introduce stored/reflected XSS. Recommend building it through the template's normal parameter substitution instead.apps/otp_provider_email/lib/Listener/SendOneTimePasswordEventListener.php
Regular issues
-
ISharepublic interface gains two new abstract methods (getOneTimePassword/setOneTimePassword) — a BC break for any third-party code implementingISharedirectly rather than extendingOC\Share20\Share. Appropriately gated behind@since 35.0.0, but worth calling out explicitly in the changelog/upgrade notes.lib/public/Share/IShare.php -
l10n placeholder bug —
createShare()uses%s-style interpolation ('No OTP provider found for id %s'), but Nextcloud'sIL10N::t()needs{}-style named placeholders, as the siblingupdateShare()correctly does with{provider}. The error message won't interpolate for end users as written.apps/files_sharing/lib/Controller/ShareAPIController.php:130(compare to line 192) -
DI inconsistency —
createShare()instantiatesnew Randomizer()directly instead of using the injected$this->randomizer, whileupdateShare()uses the injected instance correctly. Harmless functionally but hurts testability/mockability.apps/files_sharing/lib/Controller/ShareAPIController.php:136 -
JS operator-precedence bug —
if (!response.status === 200)evaluates!response.statusfirst, so it's always comparing a boolean to200. Currently harmless since axios throws on non-2xx, but it's dead/incorrect logic.apps/files_sharing/src/mixins/ShareRequests.js:614 -
Test gaps — the single-use OTP invalidation in
ShareController::authSucceeded()(clearing the OTP after successful auth) has no dedicated test, nor does theOTPProviderNotFoundExceptionpath inShareOTPController::request()(see above) or concurrent/overlapping OTP requests.
|
@miaulalala Thanks so much for the review! Most of it makes perfect sense to me and I will start implementing the requested changes shortly, but I have some questions: Regarding:
I was under the assumption that routes without the Regarding:
That (admittedly poorly named) argument expects actually not a plaintext password but a password hash. Setting a plaintext password here will cause the password validation to fail because that expects a hash. Because of this, this is not really a security issue in my opinion, but still not great. I'll drop the argument entirely (alongside the Regarding:
That's interesting. I remember trying |
|
And I just noticed something else: Since requesting the OTP is implemented in the template The base implementation should return an error (401 or 404) and the actual logic for selecting an OTP should lie with the implementing child class, but since the pubblicshareauth template is outside of the control of the application, I think it makes sense to indicate the existence and the interface of the endpoint by having a method to overwrite. Let me know whether you agree. |
1c2d696 to
271c553
Compare
|
@miaulalala I have now implemented the requested changes apart from those that aren't yet fully clear to me: Security-related
Regular issues
|
f583d80 to
e1b1aa5
Compare
|
Another day, another rebase (significantly reducing the changed files in dist/, this time) 🙃 |
e1b1aa5 to
049e0bf
Compare
…ords Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
…ackend/API) Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
…ge (if available) Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
Signed-off-by: Tobias Knöppler <tobias@knoeppler.org>
049e0bf to
faf1609
Compare
|
Sorry for the new pipeline failures. Apparently I introduced them during merge. I rebased again and fixed the issues (hopefully, couldn't test all of them reliably). |
Summary
This PR adds one-time password management to Nextcloud server and integrates them with the files_sharing app.
(the actual form in the screenshot is part of #61733)
Notes
make build-js-production).IShare->isPasswordProtected()(Refactor: Centralize logic for checking if a share is password protected #61946)TODO
Architecture and Rationale
General concepts
OTPs (one-time password) are short-lived, single use credentials sent to users via an (according to a given threat model) trusted channel (e.g. a specific email address).
OTP Providers define a method of sending OTPs to users.
OTP Recipients are valid address definitions within the scope of an OTP provider that can be sent OTPs.
Core/Server Changes
Generic
One-time passwords are implemented with generic interfaces so that they can be used by other parts of Nextcloud than sharing.
The core functionality for one-time passwords is implemented within the \OCP and \OC namespaces. OTPs are stored within a new database table
one_time_passwordand have a providerID, a recipient string, an expiration date and a password. The idea here is, that the OTP configuration (i.e. provider+recipient) can be long lived, while the credentials (password+expiration date) are (re-)generated per use.Management of OTPs is implemented in
\OC\OneTimePassword\Manager(implementing the injectable interface at\OCP\OneTimePassword\IManager).\OCP\Security\PasswordContexthas been extended by anOTPcase to allow the creation of password policies specifically for OTPs.Events are used to allow apps to register OTP providers. They need to hook into the
GetOneTimePasswordProvidersand theSendOneTimePasswordevents to provider their functionality. Providers also need to implement the interface\OCP\OneTimePassword\IOneTimePasswordProvider, which defines methods that allow theManagerto select providers and provide information about them.Sharing specific
Shares (see
\OCP\Share\IShare) have been extended with anone_time_passwordfield.The
\OC\Share20\Managerhas been adjusted to prioritize OTPs when checking the authentication for a share.The template
publicshareauth.phphas been adjusted to receive and display OTP related information (used in #61733 to show the OTP specific password form).files_sharing Changes
The
ShareAPIControllerhas been extended to allow creating and updating OTP protected shares and returning the otp configuration when fetching shares. OTPs and passwords are mutually exclusive and an error will be returned when attempting to create a share with both.The
ShareControllerhas been extended to supply template responses for public shares with otp related information.A new
ShareOTPControllerhas been implemented that allows users to request OTPs for a share.OTP Providers
An OTP provider, which allows sending OTPs via email has been implemented as (core) app:
otp_provider_email. It can be disabled or restricted by administrators to disallow this functionality.UI changes
The authentication page for public shares has been updated to accommodate OTPs (see screenshot).
When an OTP is required for a share, the authentication form will change to present a button for requesting an OTP in addition to the normal password field. It will also display a different title above the password field.
Password authentication page for OTP protected shares
The email template for sending OTPs to users is located in
apps/otp_provider_email/lib/listener/SendOneTimePasswordEventListener.phpand uses Nextclouds usual Mailer interface for templating emails.Example OTP email sent to users
The logo is not displayed, because I'm testing with a local Nextcloud instance at a non-public URL.
The custom permissions in the sidebar sharing tab (files->sidebar->sharing->custom permissions) has been adjusted to allow choosing between no password protection, setting a normal password and configuring an OTP. The descriptions of the OTP are defined and localized by the OTP provider.
New UI for selecting an authentication method during share creation
Previous UI for selecting an auth method for comparison
Checklist
3. to review, feature component)stable32)AI (if applicable)