-
Notifications
You must be signed in to change notification settings - Fork 81
Add support for Multi Factor Authentication using TOTP #2229
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
base: main
Are you sure you want to change the base?
Conversation
Hi @sapandiwakar and @pulkit110 Thanks for the PR! I've had a quick look over it and things look very good. I'll give it a full review shortly. |
Hi @joshk, thanks for the quick look! Let me know if you need anything from me to help get this ready. |
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.
This is very cool, and heading in a good direction.
I still need to try this out locally for a full review.
One question I have is how this will impact API auth?
lib/nerves_hub/mfa/user_totp.ex
Outdated
field(:secret, :binary) | ||
field(:code, :string, virtual: true) | ||
|
||
belongs_to(:user, NervesHub.Accounts.User) |
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.
could you please alias NervesHub.Accounts.User
and then change this to just User
lib/nerves_hub/mfa/user_totp.ex
Outdated
|> validate_required([:code]) | ||
|> validate_format(:code, ~r/^\d{6}$/, message: "should be a 6 digit number") | ||
|
||
code = Ecto.Changeset.get_field(changeset, :code) |
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.
since you have imported Ecto.Changeset
, you can use get_field
without the module
lib/nerves_hub/mfa/user_totp.ex
Outdated
code = Ecto.Changeset.get_field(changeset, :code) | ||
|
||
if changeset.valid? and not valid_totp?(totp, code) do | ||
Ecto.Changeset.add_error(changeset, :code, "invalid code") |
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
lib/nerves_hub/mfa.ex
Outdated
@@ -0,0 +1,96 @@ | |||
defmodule NervesHub.MFA do |
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 we should move this to NervesHub.Accounts.MFA
lib/nerves_hub/mfa.ex
Outdated
The MFA context. | ||
""" | ||
|
||
import Ecto.Query, warn: 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 don't think the warn: false
is needed
lib/nerves_hub/mfa.ex
Outdated
""" | ||
def regenerate_user_totp_backup_codes(totp) do | ||
{:ok, updated_totp} = | ||
Repo.transaction(fn -> |
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.
transaction
is no longer recommended for use, transact
is the preferred option.
(I also realize that we use transaction
everywhere)
user_id = get_session(conn, :mfa_user_id) | ||
|
||
if user_id do | ||
form = Phoenix.Component.to_form(%{}, as: "mfa") |
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 believe you can just use to_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.
Thanks, but it doesn't seem to work without it - controllers don't have a Phoenix.Component
import. It is also being used here:
form = Phoenix.Component.to_form(%{}, as: "User") |
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.
Ah, ok, thanks for checking.
timestamps() | ||
end | ||
|
||
create index(:user_totps, [:user_id]) |
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 this be a unique index?
@@ -0,0 +1,95 @@ | |||
defmodule NervesHubWeb.MFAControllerTest do |
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 use https://hexdocs.pm/phoenix_test/PhoenixTest.html where possible. Could you please look at our other controller tests, like OAuthControllerTest
, switch to this setup.
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 just noticed that some of your view tests do use PhoenixTest
, is there a reason you aren't using it here?
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 noticed you were using PhoenixTest
for the view tests, but the controller tests had already been prepared earlier. We'll update them.
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.
Awesome, thank you!
user_totp = MFA.get_user_totp(user) | ||
|
||
if user_totp do | ||
MFA.delete_user_totp(user_totp) |
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.
Since delete_user_totp
uses Repo.delete!
under the hood, if there is an error it will bubble up and cause the LiveView to crash, and not displaying any sort of error message to the user.
I would suggest not using the !
variety of delete
, and handle this use case.
Oh, and final review comment, could we update the MFA UI to use a more PIN style UI, eg. https://preline.co/docs/pin-input.html |
CR feedback
fix credo warnings
Other minor changes related to MFA
Thanks for the detailed review, @joshk. We’ve addressed all your review points except the last one (MFA UI using a more PIN-style UI), since we have a few questions:
|
Hey @pulkit110 Thanks for your awesome work on the PR and addressing our feedback.
Yep, that is more than perfect. We will be switching the other UI over soon, but its the login flow that I care most about.
Although I was using preline as an example, it was more for the UI than a recommendation to include the library. I'm hesitant to include preline if its just for this one thing. Are you able to look into 'copying' some of the sugar it provides and LiveView-ize it? You can then comment here on the pros and cons of this approach and we can decide on the best path forward. You are a ⭐ |
Thanks for your thoughtful feedback and kind words @joshk. |
@sapandiwakar I'll give this a full review shortly, but I wanted to say that the UI change is awesome! One question we still need to address is what this means for API/CLI auth? |
Ah, yes. Forgot to answer that. MFA is not enforced on API side. I think that's the standard practice as well when token based with is used. Please feel free to let us know if you'd like to change something on this side. |
This PR allows users to configure multi factor authentication using the TOTP method using NimbleTOTP under the hood.
Here are some screenshots:
User Account:

Login:
