-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
mws authentication #8596
mws authentication #8596
Conversation
Run & review this pull request in StackBlitz Codeflow. |
Confirmed: webplusai has already signed the Contributor License Agreement (see contributing.md) |
Thank you @webplusai this is looking good. I will provide some detailed feedback shortly. |
Following along (very busy at work atm, company is reorging... again). This is exciting stuff! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @webplusai this is looking good. There is what looks like some work-in-progress areas such as handleLogin. It makes a lot of sense to keep things simple there temporarily, and so I would like to merge this. However I think it may just be worth you adding comments to tag the TODO areas to make explicit the areas that we need to revisit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @webplusai that's great. I think the best way forwards now is to merge this and discuss further refinements. However at the moment the tests are failing, if you could kindly fix them up then I'll go ahead and merge.
@Jermolene I've fixed the test issue. Also some issues that I had related to the user list and roles were also fixed. |
Thank you @webplusai. As discussed I am keen to get this merged. There are some points I would like to return to but we can worry about those later. I think the blocker now is that we've got users who are testing the multi-wiki-support branch, and it is going to be a bad experience for them if they upgrade and then are presented with a login dialogue, with no obvious way to create or access a user account. Ideally, we should get to the point where by default the user gets the previous experience of anonymous read/write access, with an obvious route to get to an admin screen so that they can enable password access and try things out. One possibility would be to add commands like Let me know if that makes sense, ideally we should discuss the details before spending too much time on implementation. |
@Jermolene I've added the commands as you indicated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did only have a closer look at the code, where I did add some comments. I will have a closer look soon.
return "Usage: --mws-add-user <username> <password> [email]"; | ||
} | ||
|
||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the usernames and passwords should never be stored with the content. So for me !$tw.mws.store.sqlTiddlerDatabase
looks like it is in the same database file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @pmario I think it's perfectly OK for this data to be in the same database. There's no more obvious way for queries of the tiddler database to accidentally start returning results from the user database than there is for any data corruption within a program.
What I do want to pay attention to is the best practice of making the admin UI run off of a different HTTP host/port, so that administrators can use external tools to lock down admin access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this will prevent us from using different authentication and authorisation services / mechanisms in the future.
Using Usernames and passwords is notoriously unsecure and imo outdated already today. The minimal requirement I would consider somewhat save is username/password in a combination with a time based 2-factor-code generated with an authenticator app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan is to have pluggable authentication schemes. Form-based authentication is still pretty standard for website logins and so I don't think we can avoid offering it, but I definitely want to support things like logging in with Google or GitHub credentials.
|
||
var username = this.params[0]; | ||
var password = this.params[1]; | ||
var email = this.params[2] || username + "@tiddlywiki.com"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If username + @tiddlywiki.com
is a placeholder, imo it should be example.com
-- Otherwise it means, that tiddlywiki.com also provided an email-service. Which is not the case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps use example.com
for the moment?
var username = this.params[0]; | ||
var password = this.params[1]; | ||
var email = this.params[2] || username + "@tiddlywiki.com"; | ||
var hashedPassword = crypto.createHash("sha256").update(password).digest("hex"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simple sha256 hashing the password is not enough. The password needs to be salted and then encrypted at least 10000 times. Key-stretching makes it harder to use rainbow-tables and brute force password attacks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm comfortable with having a placeholder implementation to begin with. At this point, I think clarity is the most important consideration, and with adequate encapsulation it will be straightforward for us to revisit the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is, that it should be done right from the beginning, because the salt
has to be stored with the user data. It also plays into the login form. So there are at least 3 different positions, where a change here causes changes up to the UI password handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pmario the challenge is that we need to merge this PR to get feedback. If we wait until we've got a production ready authentication system then this PR would have to stay open for a long time, making all other work on MWS much more complicated. At this point we have the freedom that if required MWS can nuke the user database on an upgrade. We will lose that freedom as MWS moves beyond the experimental stage, so we should take advantage of it now to improve our overall velocity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
@webplusai ... Is there some description which mechanisms are used with the whole authentication system? @Jermolene ... As I wrote sensible data should not be part of the same database as the content. They should be completely separated. IMO this will also help to use other systems that provide secure vaults in the future. |
Is this order right, or did I do it wrong? Which does start the server and I do get the user UI. But if I try to create a new bag, I do get a server-runtime error
|
Thanks @webplusai Great to have your feedback @pmario, this is a great stage to be hammering out some important details. |
@@ -0,0 +1,10 @@ | |||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only 1 empty line is needed after the tiddler-header
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please check all you files to use tabs for indentation.
Command to create a permission | ||
|
||
\*/ | ||
(function(){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiddlywiki code uses tabs for indentation. Tab width = 4
The first level inside the (function(){
wrapper is not indented. So eg: export.info = ...
does not need a leading tab.
@@ -301,6 +302,22 @@ Server.prototype.addRoute = function(route) { | |||
this.routes.push(route); | |||
}; | |||
|
|||
Server.prototype.verifyPassword = function(inputPassword, storedHash) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should authentication be part of this file. IMO it should be it's own file and it should be required. So we get a clear separation of concerns. It should also help to keep the api cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a good idea @pmario
"bag-tiddlers": JSON.stringify(bagTiddlers) | ||
(function () { | ||
|
||
/*jslint node: true, browser: true */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first level inside the wrapper function should not be indented. Otherwise the diff does not show, what really changed
|
||
\*/ | ||
(function() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation
}); | ||
} else { | ||
response.writeHead(400); | ||
(function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation -- please check all files
@@ -0,0 +1,123 @@ | |||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles | |||
|
|||
\define add-role-actions() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use tabs instead of spaces for indent - please check all files
@pmario you did it correct. There was an error and I fixed it. |
As long as the servers are used on private networks only. But that's not what our users request. They request access from everywhere. So they want a simple way to connect their private servers to the internet. Connecting servers with valuable data to the internet is dangerous. See: https://techcrunch.com/2024/10/14/2024-in-data-breaches-1-billion-stolen-records-and-rising/ Hacking vulnerable devices is simple. The Mirai botnet, where the source code is here at github, only uses 64 hardcoded passwords to hijack 600.000 devices. See: https://blog.cloudflare.com/inside-mirai-the-infamous-iot-botnet-a-retrospective-analysis/ -- IMO this is a strong proof, that users do not enable security if they are not forced to. The article at cloudflare is from 2017 and Mirai should not be a thread anymore.
That's right and we should think a bit more about it. https://www.cisa.gov/resources-tools/resources/secure-by-design and https://www.cisa.gov/sites/default/files/2023-10/SecureByDesign_1025_508c.pdf The PDF above is mainly addressing companies, but I think the principles are worth considering. Especially in a TiddlyWiki context of education, where we are really strong: See: https://www.cisa.gov/sites/default/files/2023-08/K-12_Acquisition_Guidance.pdf (only 2 pages) |
@Jermolene @pmario |
That is one of the things that users want. But it doesn't really advance your case.
It is also ubiquitous. I really don't see the relevance of this comment. We're talking here about the onboarding experience of installing and setting up a new server. "Valuable data" only enters the picture once it is set up as wanted.
What is the relevance of this observation?
Again, what is the relevance of those PDFs to your argument?
Again, what bearing does this have on your argument. The position at the moment is that I proposed a way forward for @webplusai and you've raised a vague concern about "security by default" without providing any actionable feedback to the proposal. |
I think @pmario's concerns need to be taken out of the critical path of your work. I would suggest proceeding along the lines I proposed. Remember that we're trying to do minimal work to be able to merge this with a reasonable user experience. Making it anonymous access by default matches the current configuration and so is categorically not a problem. We can't hold up merging your work until some hypothetical point has been reached, we need to be practical. |
@Jermolene so just to confirm, we will proceed on this way. Right? |
Again, we need to look at this through the lens of what is the path that allows us to merge this PR as swiftly as possible. I think it's perfectly OK for the out-of-the-box experience to be anonymous read/write access, with either a web or command line path to setting up security, whichever will be quickest to build. |
By default, the Multiwiki Server starts up without authentication enabled. In order to enabled authentication, start the app using the following command: | ||
|
||
```bash | ||
npm run start -- --mws-use-auth |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the first --
for?
@@ -55,4 +56,8 @@ ServerManager.prototype.createServer = function(options) { | |||
return server; | |||
} | |||
|
|||
ServerManager.prototype.toggleServerAuth = function(shouldUseAuth = false){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the name of the function is not right. IMO it should be setServerAuth
.
I have implemented a flag that allows the app to toggle on authentication. By default, when the user starts the app using the Would you check and let me know if this is what you want @Jermolene ? |
I don't think that is the right way to handle anonymous access. Users will need to be able to configure wikis independently, so that one may have anonymous read access, one may have both anonymous read and write access and another may be entirely access controlled. I think that means that we can't have a server-wide switch that switches authentication on or off. We just need an access control system that allows a resource to be marked as permitting or blocking anonymous access. |
Hi @pmario while I don't think the concerns that you raised should block us from merging this PR, they are still relevant and I'd like to pick them up in a separate discussion thread here on GitHub. I am happy to copy across the relevant text from this thread, or perhaps it would make sense for you to initiate the discussion? |
I think security is a broader topic. So I will create a new discussion thread instead of an issue. So we can create new actionable issues from the discussion. |
@Jermolene Do we need to keep track of which user owns/created which resource? |
That would certainly be useful but it shouldn't block merging this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just some thoughts.
I think the API should be versioned. Which makes it much easier to change it in the future and also can make it backwards compatible to run more versions at the same time
@@ -641,7 +641,7 @@ NavigatorWidget.prototype.handleManageACLTiddlersEvent = function() { | |||
var paths = pathname.split("/"); | |||
var recipeName = paths[paths.length - 1]; | |||
var bagName = document.querySelector("h1.tc-site-title").innerHTML; | |||
window.location.href = "/admin/manage-acl/"+recipeName+"/"+bagName | |||
window.location.href = "/admin/acl/"+recipeName+"/"+bagName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the API paths should not be so specific. eg: /admin/
is very specific.
I would suggest /api/v1/acl/*
as base path. Which would make it consistent and it implies "admin" so it can be skipped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bear in mind that our objective with this PR is to find the shortest path to enable us to merge it. These considerations are something that we can come back to.
|
||
exports.method = "POST"; | ||
|
||
exports.path = /^\/admin\/delete-acl\/?$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above: /api/v1/alc/<resource>
-- I am not sure if the path needs to contain the action delete
since it is defined by the HTML command DELETE -- IMO we only need to specify the resource that should be deleted.
|
||
exports.method = "GET"; | ||
|
||
exports.path = /^\/admin\/acl\/(.+)$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GET /api/v1/acl/
... imo would be more consistent as it is now
(post deleted; I was inadvertently commenting on an earlier commit) |
Apologies @webplusai I've deleted my previous comment; I was inadvertently using a previous commit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @webplusai there are some issues below that I would like to resolve, but I should first once again congratulate you on this PR. It is a huge piece of work and you've achieved a very high standard. But what I really appreciate is how much you've been able to learn of the inwardness of TiddlyWiki itself. The templates you've created are a great example.
|
||
var crypto = require("crypto"); | ||
|
||
function Authenticator($tw) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In TiddlyWiki, $tw is a global that is automatically available in every module. It should not be passed around as a parameter.
if(!(this instanceof Authenticator)) { | ||
return new Authenticator($tw); | ||
} | ||
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use a global reference to the database? I think it would be better passed as a parameter to the constructor.
4. Access Control List (ACL) | ||
|
||
## Running the App | ||
By default, the Multiwiki Server starts up without authentication enabled. In order to enabled authentication, start the app using the following command: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this command is needed. I would expect to enable authentication by applying access controls to a resource that requires authentication.
@@ -44,7 +44,8 @@ NavigatorWidget.prototype.render = function(parent,nextSibling) { | |||
{type: "tm-fold-tiddler", handler: "handleFoldTiddlerEvent"}, | |||
{type: "tm-fold-other-tiddlers", handler: "handleFoldOtherTiddlersEvent"}, | |||
{type: "tm-fold-all-tiddlers", handler: "handleFoldAllTiddlersEvent"}, | |||
{type: "tm-unfold-all-tiddlers", handler: "handleUnfoldAllTiddlersEvent"} | |||
{type: "tm-unfold-all-tiddlers", handler: "handleUnfoldAllTiddlersEvent"}, | |||
{type: "tm-manage-acl", handler: "handleManageACLTiddlersEvent"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The navigator widget is part of the TiddlyWiki core. It cannot include components that are specific to MWS.
In this case, rather than using a message handler, the most appropriate technique would be for the MWS client plugin to provide a global function that yields the URL of the appropriate admin page.
title: $:/plugins/tiddlywiki/multiwikiserver/templates/add-user-modal | ||
|
||
\define add-user-actions() | ||
<$action-sendmessage $message="tm-server-request" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be missing something obvious, but where is the definition of the tm-server-request message handler?
"all-roles": JSON.stringify(allRoles), | ||
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id === user_id ? "yes" : "no", | ||
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id == user_id ? "yes" : "no", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually use ===
... Is it not possible here?
</ul> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<!-- <$reveal type="match" state="is-current-user-profile" text="yes"> --> | ||
<$list filter="[<user-is-admin>match[yes]] [<is-current-user-profile>match[yes]]"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO you should use the conditional shortcut here: https://tiddlywiki.com/#Conditional%20Shortcut%20Syntax
If the list-widget is used as an IF construction, we use the variable="ignore"
or variable="null"
because by default list-widget modifies the currentTiddler
variable, which may not be desired. -- Just a thought
@@ -58,7 +56,7 @@ GET /admin/users/:user_id | |||
"user": JSON.stringify(user), | |||
"user-role": JSON.stringify(userRole), | |||
"all-roles": JSON.stringify(allRoles), | |||
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id == user_id ? "yes" : "no", | |||
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id === parseInt(user_id) ? "yes" : "no", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseInt()
needs a radix as a second parameter. So it should be parseInt(user_id, 10)
otherwise there will be wired problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TW has a built-in utility function $tw.utils.parseInt()
that wraps parseInt.
Thank you @webplusai I think we are at a good place to merge this. There's a lot more to do to clean things up and improve the user experience, but I think it's going to be much easier to make progress after merging this. Again I really want to congratulate you on your work here. TiddlyWiki is a somewhat non-standard environment and I'm delighted to see how you have been able to adapt to it and take advantage of its features. |
No description provided.