In part 1 we walked thru the installation and customization of Phx.Gen.Auth. In today’s post we’ll continue with some customizations; we’ll add the ability to block user accounts, and we’ll implement simple Bamboo email notifications.
Getting started
If you’ve been following along you can continue with the code from part 1, or you can grab the code from GitHub.
Clone the Repo:
If cloning from GitHub:
Terminal
Let’s create a branch for today’s work:
Terminal
And make sure we have our dependencies and database set-up.
Terminal
Terminal
Note: before running ecto.setup
you may need to update the username / password database settings in dev.config
to match your local postgres settings, i.e.
/config/dev.config …line 69
Terminal
With that out of the way let’s get to blocking some users!
Adding the ability to block a user
The first step in our implementation is to add a new column to the users
table… so we’ll need a migration.
Adding the migration
Terminal
/priv/repo/migrations/timestamp
_add_is_blocked_to_users.exs
Pretty simple, we add a new column with a default of false
. We’ll use this column to indicate whether a user is blocked or not. The default value of false
ensures any existing users will not become blocked when the migration is run. Likewise the default ensures new users won’t be blocked on registration.
Let’s run the migration.
Terminal
Next we need a quick update to the user
schema.
Updating the user schema
We’ll add a field for the new database column, and in anticipation of the change required to the accounts
context, also a new is_blocked?
function. Similar to the is_confirmed?
function from part 1, this could just be implemented as an attribute check in accounts
… it’s pretty much a stylistic choice as to which you prefer.
/lib/auth/accounts/user.ex
Onto the context!
Updating the accounts context
This is another simple change, we’ll add a new condition in get_user_by_email_and_password
to check if the user has been blocked.
/lib/auth/accounts.ex
Finally we need a controller update.
Updating the user_session controller
We need to add a new match for the {:error, :user_blocked}
tuple which we added to the context.
/lib/auth_web/controllers/user_session_controller.ex
And with that… we’re done… well not really, but we have a simple first pass. Let’s do a quick run thru of our new feature.
Seeing the changes in action
Fire up the server:
Terminal
Now we’ll manually update the is_blocked
field of an existing user in TablePlus and attempt to log in.
Fantastic, everything works… maybe we really are done!
Alas, not so. If we set is_blocked
back to false for bob@example.com
; login; and then set bob
once again to blocked, you’ll notice bob
can still access the application. This is down to the session token still existing.
When a user logs in, a session token is created for them, we need to deal with this as part of the block functionality. If we don’t take the session token into account, a user who is already logged in won’t actually get blocked until they log out and then attempt to log back in.
So we need to remove the user_token
row from the database when we block a user. Given there are a couple of steps involved in blocking a user, we’ll create block and unblock functions. To keep things simple we won’t bother creating a UI for the blocking functionality as this would probably go in some sort of admin interface which we don’t have. Let’s see what the backend methods look like however.
Implementing the block and unblock functions
We need a couple of new functions in accounts.ex
:
/lib/auth/accounts.ex
Nothing too tricky, the unblock_user
function is very simple, we just call into Repo.update
with a block_user_changeset
which we’ll create momentarily. block_user
is slightly more complicated as we need to remove those pesky tokens. We make use of an Ecto.Multi
to update the user and then delete the tokens.
We now need to add the block_user_changeset
function we’re using in the block / unblock functions.
/lib/auth/accounts/user.ex
This function just flips the is_blocked
flag based on the input parameter.
With these changes we should be all good.
We’ll run the server in interactive mode and see what happens when we fire off our functions.
Terminal
We’ll log in with Bob and navigate to our secured page.
Now let’s block Bob by typing the following into the iex
session.
Terminal
Attempting to access a secured area of the application now results in Bob being booted to the log in page, as he has been logged out, perfect!
… and we get the expected result when attempting to log back in.
Let’s try an unblock
.
Terminal
As expected Bob
can now log in.
The functionality is looking good, but we have some testing to do, let’s get at it!
Updating the tests
Context tests
We’ll tackle a few things when it comes to the accounts
test:
- The new logic around
get_user_by_email_and_password
. - The
block_user
function. - The
unblock_user
function.
We’ll start by adding a test in the get_user_by_email_and_password
describe block to ensure we don’t return a user when they are blocked.
/test/auth/accounts_test.exs
Simple enough, we just block the user created by our fixture and then check for an appropriate return tuple from get_user_by_email_and_password
.
Now let’s add a test for the block_user
functionality:
/test/auth/accounts_test.exs
In the setup
block we create both a user and a token. The test itself verifies is_blocked
is set to false
, and that the token is removed.
The unblock_user
test is very simple:
/test/auth/accounts_test.exs
In the setup, we create a user, block them and then in the test itself, unblock them and check the is_blocked
flag is false.
Controller tests
Since we haven’t added any UI components for block_user
or unblock_user
, we have nothing to do in terms of controller tests when it comes to these functions. We do want to add a user_session_controller
test for what occurs when a blocked user attempts to log in however. We’ll add this after the existing "emits error message when account is not confirmed"
test.
/test/auth_web/controllers/user_session_controller_test.exs …line 68
Simple, we block the user and ensure we get the expected response when attempting to log in.
Running the tests, everything is passing.
Terminal
That does it for block / unblock, before ending our tour of Phx.Gen.Auth
let’s swap out the console notifier with a simple Bamboo
implementation.
Swapping out the console notifier
We’re not going to do anything complicated here, in a real implementation you’d probably want to put together some nice looking emails, maybe making use of MJML, but we’ll keep it basic.
Our email package of choice will be Bamboo
. It’s a great way to send email and comes with many adapters for various email services. It also includes a local adapter, which will work great for our purposes… so let’s get to it!
Adding Bamboo
We need to add Bamboo
to our mix.exs
file and grab the dependency.
/mix.exs
Terminal
In terms of configuration, we’ll stick with just the dev
config. For a production application you’d need to configure your prod
config to include the settings for the adapter you’re looking to use.
/config/dev.exs
And when I said we’ll just add a dev
config, it turns out I lied, we’ll also need a configuration entry in test.exs
so the tests continue to run. Bamboo comes with a handy TestAdapter
just for this purpose.
/config/test.exs
Finally in order to display our emails when running locally we need to update router.ex
. I’ve placed the Bamboo
specific route code under the LiveDashboard
section.
/lib/auth_web/router.ex …line 37
The SentEmailViewerPlug
is what will display emails when running in development.
Now we need to create a simple Bamboo
mailer module.
Terminal
/lib/auth/mailer.ex
Nothing complicated, this is right from the Bamboo
documentation.
Finally, we can replace the existing user_notifier.ex
code with the below.
/lib/auth/accounts/user_notifier.ex
This code is pretty simple, just consisting of text and html email templates which get sent via Bamboo
in the deliver
function.
Now we can navigate to http://localhost:4000/sent_emails to see any emails that are being sent.
Scrolling down you’ll see the text version:
So our notifications are all good!
If you run the tests however, you’ll notice a number of failures. This is due to a function in accounts_fixtures.ex
which is used to extract tokens from the body of the notifications.
For example there is a test that ensures the user can be retrieved based on a reset password token, the set-up of this test extracts the token like so:
/test/auth/accounts_test.exs
The structure of the notifications has changed slightly with the updates we made to user_notifier.ex
. As a result we need to update the fixture code.
/test/support/fixtures/accounts_fixtures.ex
We’ve just swapped out captured.body
for captured.text_body
as that is now the key we’re using in user_notifier.ex
.
And that does it! All our planned functionality has been implemented and our tests are passing.
A few things to be mindful of
There are a few things to be mindful of when it comes to the customizations we’ve made.
Email discovery
We’ve opened up the potential for email discovery since we now present different messages on unsuccessful login attempts.
For instance the message we display for a non-confirmed user is different than what we display if an incorrect email or password is entered. In theory this means it’s possible for someone to find the email of users in our system who have registered but not yet confirmed their account (the same applies to blocked users). One could use a web scrapper or the like to hit the login page with a bunch of random emails and make note of any emails that displayed the “Please confirm your email before signing in…” message.
Multiple confirmation emails
Since we re-send the confirmation email when someone attempts to log in prior to confirming their account, we could in theory have a user repeatedly log in without confirming their account and potentially spam someone with email (not to mention run up the amount of emails we’re sending thru our chosen email service). For instance if I register with an email other than my own and then keep attempting to log in, whoever actually owns that email account is going to get a bunch of confirmation emails.
I think the benefits of presenting useful error messaging and re-sending the confirmation email outweighs the potential downsides but both these issues are something to be aware of.
Summary
That’s it for our walkthru of Phx.Gen.Auth
, it’s great to have another authentication option available for Phoenix, options are always good!
Thanks for reading and I hope you enjoyed the post!