We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Related
Jump to file
- ∟ mix.exs
- ∟ config/config.exs
- ∟ lib/my_app/accounts.ex
- ∟ lib/my_app/accounts/user.ex
- ∟ lib/my_app/accounts/user_token.ex
- ∟ lib/my_app_web/live/user_settings_live.ex
- ∟ lib/my_app_web/live/user_verify_totp_live.ex
- ∟ lib/my_app_web/router.ex
- ∟ lib/my_app_web/user_auth.ex
- ∟ priv/repo/migrations/20240424232433_alter_auth_tables_add_totp.exs
Install deps
Install the following dependencies to get started. You can find their documentation on HexDocs.
- NimbleTOTP
- hexdocs.pm/nimble_totp
- EQRCode
- hexdocs.pm/eqrcode
Configure TOTP
The "issuer" we configure here will be what shows up in user authenticator apps. Without it, they would not be able to tell the user which code is for which app.
Set up accounts context
There are a few capabilities that we need to add to the
Accounts
context (or
Users
context, depending on your project).
First, we must generate a new TOTP secret for a user that wants to set up 2FA. This secret is generated by
NimbleTOTP
, we need it to generate and verify the familiar six-digit codes for users.
This is done by
totp_secret()
, and will be stored in the
User
struct later on.
We also need to know what state a user is in, regarding their TOTP status. We'll do this with two functions:
totp_enabled?(user)
and
totp_verified?(user, opts)
.
A user has TOTP "enabled" if we've assigned a secret to them. Posessing a secret implies that they
have enabled and verified it, as we otherwise would not save the secret to their record.
Checking if a user is currently "verified" with respec to TOTP involves a few checks.
We check if they have TOTP enabled at all, and simply return
true
if they do not.
If TOTP is enabled, we check if their current session has ever verified successfully.
Finally, if they have verified at some time in the past, we can check to see if that date is
within some duration. Here, we use a 24 hour period before verification is no longer valid.
So, how do we actually go about enabling TOTP? To do that we need to build an auth URI, and then
encode that as a data URI that will render a QR code. This is what TOTP apps scan in order to obtain
your secret, and generate codes.
You can see how this works in
totp_data_uri(user, totp_secret)
.
Next, we'll need some way to verify that a TOTP is correct, prior to enabling it for a user.
We'll use
NimbleTOTP.valid?/3
to check validity in our
verify_session_totp(token, totp_secret, verification_code)
function.
This function accepts parameters that allow us to check that a code is valid for the given secret,
and was generated no more than 30 seconds ago. This time restriction is important, as we don't want to
accept old codes.
The function also updates the current session with the time of verification. Note that this value
is stored on the session and not the user record. This means that new sessions will be required to
go through TOTP verification.
Now, to enable TOTP officially for a user, we just have to use our verification function, and set the generated secret on our user if it succeeds.
If we ever want to remove TOTP for a user, we just have to delete the stored secret. You
can see that this is exactly what's done in
disable_user_totp(user)
.
Add new fields to User schema
We have two fields to add to our User schema:
1.
:totp_secret
— Stores the TOTP secret for a user, if present this
indicates that they have enabled TOTP. Note that this is sensitive info, so include
redact: true
to omit this field value from logs.
2.
:totp_verified_at
— Indicates that time at which TOTP was last verified. This is a virtual field (does not exist on your
users
table) because it is loaded dynamically from the users current session, as you will see later.
Add new fields to UserToken schema
Here we need to add
:totp_verified_at
to our token schema. As mentioned, this will indicate when TOTP was last verified for the current session.
In the query that fetches our user by session token, we add a
:select_merge
call, which
populates
:totp_verified_at
into the corresponding virtual field in our user struct.
Enabling & disabling in user settings
Here, we create a LiveView that allows a user to enable, confirm, and disable TOTP.
We start with an "Enable" button if the user does not have TOTP enabled.
When the button is clicked, generate a TOTP secret and change the page state to
:confirming
.
Now, the user will have to scan the QR displayed with their TOTP app, and submit a valid code to move to the next step.
Once a code has been verified and the secret saved to the users account, they may opt to disable TOTP, deleting the secret from their account.
Verifying TOTP codes
To verify TOTP for a users session, we accept a code from them and verify it on
the backend. Because this updates the
:totp_verified_at
field on their session,
we can redirect them to where they wanted to go and they should now be allowed.
Authentication in your router
In your router, we'll use new plugs and on_mount hooks to authenticate against TOTP status, which you'll see later. Here, we're applying these to every route that also requires an authenticated user.
However, you can just as easily mix and match these so that some routes only require password authentication, while other, more secure, routes require TOTP. GitHub does this for accessing your settings.
You can even have routes that use varying expiration times for verification. This would require changing the provided context functions, and adding new plug functions and on_mount hooks. This allows for fine-grained control over when TOTP is required.
Authentication helper functions
UserAuth
is a great place to add authentication helpers, which
is what we do here for checking for TOTP verification. It's important to keep
these functions simple, to help prevent holes from forming in your auth logic.