-
Notifications
You must be signed in to change notification settings - Fork 39
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
Multi-factor login #236
Comments
@dantownsend It's a nice idea for a regular website, but for the admin interface I don't think it's necessary because it's too much work, and the Piccolo Admin has already limited access. When a user create an account admin argument is |
@sinisaos It's mostly a defence-in-depth in case someone brute forces the username and password on the login page. We use rate limiting, so it would take a long time, but it's possible if someone had a really obvious username and password (like admin/admin), or their credentials had been leaked somewhere. It would be a nice feature - I would definitely use it. But it is a fair bit of work. I'll leave this issue here for now, and might pick it up in the future. |
@dantownsend Good point. I didn't think about the superuser himself and his potential mistakes. In that case, multi-factor authentication makes perfect sense. It will be a great feature when it's done. |
Here's my two cents on MFA from both a user view and security view. Support multiple forms of MFAAs a user when I think of the ability to sign up with MFA, I tend to think of TOTP codes or Yubikeys. It would be nice to provide an end user the ability to sign up using their preferred MFA method. Some examples are listed below:
From a security aspect, each of these approaches come at different security risks. For example, it's a lot easier to conduct phishing on TOTP codes versus something like hardware keys which are currently considered unphishable. From a developers point of view as well, if you added support for things such as hardware keys or TOTP then it would be a lot easier to run a local development environment. I'm just thinking that if you only support email, then in order to test locally you'd always need an email backend which isn't always trivial to acquire. Allow for multiple forms of MFA at onceOften sites only allow an end user to set up one form of MFA at once which isn't great from both a usability aspect and security aspect. For example, what happens when you lose your only form of MFA? All I have to say here is I'd recommend that this is made as something like an Ensure codes expire and are one time usageFrom a security aspect, ensuring any email or SMS codes meet the following criteria is something worth seriously considering:
Other forms of MFA should also meet these expectations, but RFC compliant libraries in theory already handle that for you. A lot don't so it'd be worth checking. Ensure MFA checks count towards lockoutsAs far as I am aware Piccolo does not currently feature an account lockout policy. If that changes in the future however, it should be noted that MFA failures should count towards a form of lockout. If it does not, then MFA codes could possibly be brute forced for example. Ensure MFA modifications are treated as privileged actionsPrior clarification, a privileged action is something which in my eyes modifies an account / access to an account and should require higher verification to undertake. An example of a privileged action within the current code base is changing your password via the admin panel. This change requires you to submit your current password in order to set a new one. When implementing similar functionality for adding/changing/deleting a users MFA options it should also enter this higher privileged mode. As an example, the following is a real world attack which may be undertaken if any authenticated session can simply modify MFA sessions. Lastly, I know that adding support for these points would involve more effort then email alone however from a security aspect I feel they are definitely worth it. If I end up sometime in the future I may do some work towards this point also, but I'll use this issue as a discussion area. |
@Skelmis I didn't adequately thank you for this - it's super helpful, thanks! I'm going to take a serious look at implementing MFA. |
I've thought more on this topic also, and I'd propose the following as a base class or something similar. The idea being the user can implement any form of MFA they want, provided it implements the aforementioned API's. This would allow Piccolo to meet the constraints I outlined in my original post, while maintaining enough flexibility for future demands. Further, this would mean each type of MFA can maintain a user back reference and have custom table schemas. from abc import ABC, abstractmethod
from piccolo.apps.user.tables import BaseUser
from piccolo.table import Table
class MFA(Table, ABC):
enabled: bool
"""Whether or not this MFA type can currently be used."""
user: BaseUser
"""The back reference to the user table"""
display_name: str
"""A display name for users.
This allows them to differentiate between
multiple instances of the same MFA type.
"""
@abstractmethod
def setup_hook(self, *args, **kwargs):
"""Called when a user attempts to add this form of MFA.
In an example flow such as email, this method would
generate an email code and send it to the user before
asking them to verify by entering the code.
"""
...
@abstractmethod
def setup_verify(self, *args, **kwargs) -> bool:
"""Called by a user when attempting the verify the addition of MFA.
In the example flow, this method would be called with
the code that was emailed to the end user.
:rtype: bool
:return: True if MFA verified successfully, False otherwise.
:notes: On success, this method should add MFA to the users account.
"""
...
@abstractmethod
def login_hook(self, *args, **kwargs):
"""Called when a user attempts to log in and selects this form of MFA.
In the example flow such as email, this method would
email a code to the end user before asking them
to verify by entering the code.
"""
...
@abstractmethod
def login_verify(self, *args, **kwargs) -> bool:
"""Called by a user attempting to authenticate with this MFA.
In the example flow such as email, this method would
validate that the code provided by login_hook was used.
:rtype: bool
:returns: True if MFA verified successfully, False otherwise.
"""
... I've also decided to include some example implementations as this provides a lot of flexibility and easy of implementation. No MFAThis would be the default situation, and just provides a way inline with the proposed API to allow existing behavior. class NoMFA(MFA):
def setup_hook(self, *args, **kwargs):
"""No MFA means no prompts or anything here."""
pass
def setup_verify(self, *args, **kwargs) -> bool:
"""No MFA means this is always successful."""
return True
def login_hook(self, *args, **kwargs):
"""No MFA means no prompts or anything here."""
pass
def login_verify(self, *args, **kwargs) -> bool:
"""No MFA means this is always successful."""
return True Email basedThis form of MFA supports the ability to send user's emails and verify the code. For simplicity, this is psudocode methods etc. import secrets
from datetime import timedelta, datetime
class EmailMFA(MFA):
VALIDITY_PERIOD = timedelta(minutes=5)
EMAIL_TEMPLATE = (
"Hey {},\nHere is your verification code: {}\n\nThis code is valid for {}."
)
IS_CONFIGURED: bool = False
def generate_new_code(self) -> str:
return secrets.token_hex(4)
def code_is_valid(self, created_at) -> bool:
"""Return true if the code is still valid"""
...
def email_user(self, code):
email = self.EMAIL_TEMPLATE.format(
self.user.username, code, self.VALIDITY_PERIOD
)
...
# SMTP email or whatever here
def setup_hook(self, *args, **kwargs):
if self.IS_CONFIGURED:
...
# Tell the user this is already configured
# and to make a new one instead
self.setup_code = self.generate_new_code()
self.setup_code_generated_at = datetime.now()
self.email_user(code=self.setup_code)
...
# Have a pop-up appear asking for the code
def setup_verify(self, code) -> bool:
if self.IS_CONFIGURED:
...
# Tell the user this is already configured
# and to make a new one instead
return False
if not self.code_is_valid(self.setup_code_generated_at):
...
# Tell the user the code expired
return False
if self.setup_code != code:
...
# Tell the user the code is wrong
return False
self.IS_CONFIGURED = True
self.save()
return True # Tell user its setup
def login_hook(self, *args, **kwargs):
self.current_code = self.generate_new_code()
self.current_code_generated_at = datetime.now()
self.email_user(code=self.setup_code)
...
# Have a pop-up appear asking for the code
def login_verify(self, code) -> bool:
if not self.IS_CONFIGURED:
...
# Tell the user this is not configured
# and to configure one instead
return False
if not self.code_is_valid(self.setup_code_generated_at):
...
# Tell the user the code expired
return False
if self.setup_code != code:
...
# Tell the user the code is wrong
return False
return True # Auth the user Hopefully this all helps and you can see the benefits to a plugin based API to allow for support of multiple forms of MFA, including user supplied means. Then for end consumers, they could simply supply something like: supported_mfa_claims: list[MFA] = [...] Which Piccolo would consume and then allow users to pick anything in the given list |
I've also had a bit more of a think of this, Piccolo should also provide a way for end developers to enforce MFA via some form of setting. When set, users must have atleast one form of MFA present on their account |
@Skelmis Thanks a lot for this. I agree that allowing the user to configure which MFA they want make a lot of sense. I had a look into it a few weeks back, because I'd love to get it added to Piccolo Admin asap. The first step is figuring out which MFA to add first:
What do you think makes most sense to tackle first? |
I'd likely look to tackle authenticator apps first, this also means you can more easily conduct local development and testing within an MFA environment. Further, while this oftens falls to the users perspective email can sometimes fail to be considered multi factor (still worth implementing mind you. Any form of MFA is better then none). For a bit more of a reason as to why, here's a link to the OWASP page on it. I would recommend having backup code's implemented alongside all forms of MFA. It's likely not worth tying it to one specific form of MFA however, but rather an entire account so users can get in if all MFA fails. My knowledge of passkeys is only as an end user, however I do believe there are some open source packages to hook into. Same for TOTP mind you. For some further reading, this is a great cheat sheet I'd recommend having a read of. |
I've been playing around with two-factor authentication for a server-side rendering app. This has some mitigating circumstances because it is much easier to implement with server side Jinja templates and two-factor authentication is always enabled in that app (no option to disable 2FA). Workflow is straightforward. New user is registered, after registration new user is redirected to page with QR code. Then the user scans that QR code with Authy or some other authenticator (register account in Authy). After that user must log in and when pass valid credential is redirected to page to insert TOPT code and than user is logged in (pretty similar like Github does). 2fa.webmFirst, it's much harder to implement this with a separate backend and frontend. Even React Admin (which is, I think, the most popular partially open source frontend admin) doesn't have an official (or any other publicly available) implementation of two-factor authentication. As you can see in the posted video, everything works, but there are a few issues which I have trouble to solve.
There are probably some other problems that I haven't encountered. Sorry for the long post, but I tried to explain the difficulties I ran into and this may be useful as an example. |
@sinisaos That's very impressive - thanks! I was looking into it yesterday too. I was starting more from the database tables - you've made a lot of progress already on the endpoints, and generating the QR codes etc which is really cool. I'll try and merge both our branches.
Yeah, we'll need a database table for storing the secrets, so we know the user has MFA enabled.
This is tricky - we'll have to make the user sign in with username and password first, so we know they're a valid user, and the API response will ask them to enter a TOTP code. The front end will then show a TOTP input field, and will submit that along with the username and password to the login endpoint. I thought about having separate middleware for checking the TOTP token, but it's kind of hard to separate it from
We can solve this by using a database table, which will tell us which user has MFA turned on. |
Hrmm, that does look like it could be problematic. I have a couple ideas for this, but not a lot of time right now. I almost think the way it may need to be done looks something like the following flow from the perspective of the admin form being
This also resolves one of the security gotchas with regards to MFA because by asking the user to submit the relevant details at the same time (password, mfa) it now forces an attacker to brute force both parameters at the same time versus one at a time. As a result even if an attacker manages to brute force a password, Piccolo won't disclose that to that person. Although, it does mean the need to implement a couple separate flows within the frontend based on the auth type |
@dantownsend Thanks. Code in that branch is only example and feel free to use anything you find interesting. |
Haha all good! Security does often get in the way of getting something out the door. To which I'd also say ignoring the proposed flow is an acceptable solution tbh. I did have some time set aside in a few weeks to possibly have a bit of an initial look into things, although by the looks of things it'll be available for a bit more in depth stuff. I look forward to seeing where all this goes, it's nice to see it all coming together |
BaseUser
has anemail
field, so we could integrate multi-factor login.When a user attempts to login, after verifying the username and password, we email them a code which they need to enter into the UI.
We would need a table for storing the codes - we could create a new Piccolo app called something like
multi_factor
.We would have a base class called something like
MultiFactorProvider
, with asend_code
method. Out of the box we can have a subclass for sending email over SMTP.There are already some security measures on the login page, like rate limiting, but having multi-factor would be a great step forward.
Email seems like the best route forward. With authenticator apps, we would need to build additional UI for displaying QR codes etc. With SMS, there's no standardised API that I'm aware of for sending them, like with SMTP, so would be more work.
This is a large feature, but would be pretty awesome.
Example UI / workflow:
The text was updated successfully, but these errors were encountered: