In the past I’ve primarily used Pow as an authentication solution for Phoenix. It’s well documented and fairly easy to customize.
However, it’s always fun to kick the tires on something new. Considering Phx.Gen.Auth comes from José Valim, the author of Elixir, it seems likely it will find a fair bit of adoption within the community.
Phx.Gen.Auth
is a little different than other authentication solutions in that; as hinted by it’s name; it generates and injects the authentication code directly into your application. This blog post by José Valim provides some motivation around the thinking behind this.
In anycase, we’ll look at how to get Phx.Gen.Auth
set-up and then how to add some custom workflows and functionality.
Specifically we’ll be:
- Adding a password confirmation field to the registration page.
- Altering the registration workflow to prevent authentication prior to account confirmation.
- Adding the ability to block a user.
- Swapping the default console logger notifications with Bamboo.
I’d encourage you to follow along, but if you want to skip directly to the code, it’s available on GitHub at: https://github.com/riebeekn/phx-gen-auth-example.
Let’s get at it!
Creating the app and adding Phx.Gen.Auth
We’ll be working with a traditional Phoenix application but if you’re working with LiveView things will be pretty much the same.
Note: A good walk through and example of how to make use of Phx.Gen.Auth
within LiveView is contained in this video. Around the 34 minute mark is where consuming Phx.Gen.Auth
tokens within a LiveView app is discussed.
Let’s fire up a terminal session and create the application.
Terminal
Select yes when asked to install dependencies.
Once that’s completed we’ll head over to the GitHub page for Phx.Gen.Auth and follow the installation instructions. The installation steps are well laid out and we’ll be following them pretty much verbatim.
First we change into the directory where our application has been created.
Terminal
And now add the Phx.Gen.Auth
dependency to the mix.exs
file.
Updated 2021-11-11:
Note that Phx.Gen.Auth
has been merged into Phoenix as of version 1.6… so if you are using version 1.6 or later of Phoenix, you should skip adding it as a dependency in your mix.exs
file and just go directly to executing the mix phx.gen.auth
command.
/mix.exs … line 34
After updating the mix.exs
file, we need to grab the new dependency:
Terminal
With that accomplished, we can generate the authentication code.
Terminal
After generation we need to grab dependencies once again, and create the database for our application.
Terminal
And we now have a default installation of Phx.Gen.Auth
up and running… was that easy or what!
We can see our authentication code in action by firing up the Phoenix
server.
Terminal
When navigating to http://localhost:4000/ we’ll see we have some authentication specific links available:
Clicking the Register
link brings us to a registation page where we can set-up a new user.
After registering the user, we’ll see that we are logged in by default.
And a confirmation notification has been pushed to the iex
console.
The generator creates screens for updating the user’s settings; handling forgotten passwords; and pretty much everything we need for a basic authentication system. I’d encourage you to run thru the various authentication screens to get a feel for the workflow.
One great feature of Phx.Gen.Auth
is it generates tests as well as application code. We can kick off the tests via the terminal.
Terminal
These tests will come in handy when we start to modify the authentication behaviour in the next section.
Adding a secured page
Before customzing the Phx.Gen.Auth
code, let’s see how we can go about restricting access to parts of the application. We’ll start by creating a new template for the existing page controller.
Terminal
/lib/auth_web/templates/page/secure.html.eex
Nothing fancy, just a simply template with a bit of header text.
Let’s hook this up in the controller.
/lib/auth_web/controllers/page_controller.ex
All we’ve done is add a function to render the new template.
The "magic"
, such as it exists happens in the router.
/lib/auth_web/router.ex …line 61
Some new scopes and routes have been added to router.ex
during the Phx.Gen.Auth
set-up. To add a protected page / route we just need to add it to the scope which pipes thru :require_authenticated_user
.
If you want to keep the generated routes seperate from your custom routes, just create a new block. For instance instead of the above we could have created a new block:
/lib/auth_web/router.ex
Now when attempting to access http://localhost:4000/secured_page without first authenticating, we’ll be redirected to the Log In
page and presented with an appropriate message.
As expected, after logging in we can view the page.
Customizing Phx.Gen.Auth
Next let’s look at how we can customize Phx.Gen.Auth
.
We’ll start off with a very simple change; adding a password confirmation field to the Registration
page.
Adding a password confirmation field on registration
We need to update the new.html.eex
registration template.
/lib/auth_web/templates/user_registration/new.html.eex
All we’ve done is add a password_confirmation
label, input and error tag under the existing password items.
In user.ex
we need to make a small change to the registation_changeset
.
/lib/auth/accounts/user.ex …line 23
We’ve added a validate_confirmation
check to ensure the passwords match.
Finally, we need to update the tests affected by our changes.
Let’s start with user_registration_controller_test.exs
, we’ll make 2 changes.
/test/auth_web/controllers/user_registration_controller_test.exs …line 23
The password_confirmation
field has beed added to the map of values.
Now let’s update the test that validates the submitted data.
/test/auth_web/controllers/user_registration_controller_test.exs
We’ve again added the password_confirmation
field and added an additional assert
to check for the "does not match password"
error message.
We should also update accounts_test.exs
.
/test/auth/accounts_test.exs …line 60
Similar to the invalid data
test in the controller we’re checking for the password_confirmation
error message.
We also want to add the password_confirmation
value to the map we pass to Accounts.register_user
:
/test/auth/accounts_test.exs …line 92
Running our tests, all should be good:
Terminal
Sweet! Now when registering a user, a password confirmation field is present.
Next let’s look at updating the registration workflow. This will be a more involved change.
Updating the registration workflow
Currently user’s can access the application without confirming their account. They are authenticated immediately after signing up, and are able to log in and log out of the application. Let’s see what would be required to restrict our application to confirmed users.
Determining how authentication works on the back-end
The first thing we want to do is figure out how authentication actually works! A quick look at the user_session_controller
offers a hint.
/lib/auth_web/controllers/user_session_controller.ex
So it looks like Accounts.get_user_by_email_and_password
is the function we’re after, let’s check it out!
/lib/auth/accounts.ex
It appears the method returns a user
on success, nil
on failure. This matches what we’re seeing in the controller code and also matches what the tests indicate.
/test/auth/accounts_test.exs
Another great bonus of tests is they can help with understanding and confirming the expected functionality of an application… if you write tests, your future self and others will thank you!
With the above knowledge in hand, we’re ready to make our change.
Updating the code
We will need an additional check in get_user_by_email_and_password
to ensure the user is confirmed, so let’s add that.
/lib/auth/accounts.ex
Simple! We have a new is_confirmed?
condition that we’ll create in user.ex
.
/lib/auth/accounts/user.ex
Creating a new function here is perhaps going overboard. We could just perform the check explicitly in accounts
, for example:
But adding a function, I feel sticks closer stylistically to the existing valid_password?
check, and more explicitly states what it is we are checking for.
With the above minor changes we already have a decent first swipe at an implementation… before trying things out let’s see what’s happened with our tests.
Terminal
Whoops, as Jay-Z would say, we’ve got 7 problems and they’re all failing unit tests… actually I’m pretty sure Jay-Z never says that… and I promise no more attempts at lame jokes.
Fixing the tests
Looking at the test results, the most basic of scenarios where we expect a user to be authenticated with valid credentials fails, i.e.
This makes sense as we’ve now added the is_confirmed?
check to the validation logic. In our tests we call Auth.AccountsFixtures.user_fixture()
to get the user under test, so we need to update the fixture to set the confirmed_at
value for the user. Let’s see how that might look.
/test/support/fixtures/accounts_fixtures.ex
Essentially what we’ve done here is replicate some of the functionality from accounts.ex
that relates to confirming a user. Now when we get a user from the fixure, their account will be confirmed.
If we re-run the basic autentication test it now passes.
Terminal
How about if we run all our tests?
Terminal
Not so good! We have 4 failing tests, if we look at the details of those tests we’ll see they are related to user confirmation. So for some of our tests we need a confirmed user, for others we need an unconfirmed user. Therefore we need to add this flexibility in the fixture.
/test/support/fixtures/accounts_fixtures.ex
There is now an optional keyword list available as a second parameter on user_fixture
. In the function itself, we check for the confirmed
keyword, and default confirmed
to true
when it is not present. This means by default we’ll set our user to confirmed, but have the option of creating an unconfirmed user. In the case of the registration tests, this is exactly what we need, so we can now update them as below.
/test/auth/accounts_test.exs
/test/auth_web/controllers/user_confirmation_controller_test.exs
We’re back to passing tests.
Terminal
Fantastic!
Adding a test for our new functionality
Before moving on we should add a new test to ensure we don’t return an unconfirmed user from get_user_by_email_and_password
.
/test/auth/accounts_test.exs
When we run our tests, we should now see an additional passing test.
Terminal
How do things look in the UI?
So we have our tests sorted, let’s try out the new functionality. Logging in with an unconfirmed user yields:
Good news and bad news. The user is being refused entry to the application, but the message doesn’t make a lot of sense.
What about if we register a new user?
Yikes, that’s not good, they get authenticated on registration just like before. Looks like we still have some work ahead of us.
Updating the registration workflow (for real!)
We’ve made some decent progress, but need to address the messaging and the registration workflow.
Updating the code
We’ll ignore the tests for now and get thru the implementation, the changes are pretty simple. We’re going to need to update what we return from get_user_by_email_and_password
to include more information about what has occurred. After doing so we’ll make some controller updates.
Let’s start off by changing get_user_by_email_and_password
.
/lib/auth/accounts.ex
We’re now returning :ok / :error
tuples so we can provide specific error status atoms.
This will require us to change user_session_controller
where we call get_user_by_email_and_password
.
/lib/auth_web/controllers/user_session_controller.ex
We’ve swapped out the “if Accounts.get_user_by_email_and_password(email, password) do...
” condition for a with
statement. This allows us to handle the different reasons for why a user may be denied access. In the case where they haven’t confirmed their account, we provide appropriate feedback in the error_message
and also re-send the confirmation notification.
The only thing left to do is not authenticate the user on registration.
/lib/auth_web/controllers/user_registration_controller.ex
We’ve removed the call to UserAuth.log_in_user(user)
(the AuthWeb.UserAuth
alias can be removed as well) and updated the success message. A redirect has been added after the put_flash
statement as UserAuth.login
was handling the redirect previously.
Now if we register a user we see they are no longer authenticated upon registation.
And if we attempt to log-in prior to confirming the user account, we get both a more informative message:
… and a confirmation notification:
Note: each time a confirmation notification is sent, a new confirmation token will be inserted in the database.
This won’t be an issue however, as the tokens will all be cleaned up on confirmation, i.e.
And the tokens are gone, perfect!
We’ve made a number of changes, let’s see what happens with the tests.
Terminal
Five failures, not too bad, our last task for the day will be getting the tests sorted.
Fixing the tests
Even without looking at the failures I know the primary issue is going to be the return type change we made to get_user_by_email_and_password
. So let’s update the associated accounts.exs
tests to handle the new return type.
/test/auth/accounts_test.exs
With the updated return values, running the tests now yields…
Terminal
A single failure with the registration controller. We need to update the create
test to indicate we no longer log the user in upon registation.
/test/auth_web/controllers/user_registration_controller_test.exs …line 23
The test title has been updated to indicate we no longer log in the user upon registation. We also call refute
on get_session
to ensure we don’t have a user session after registering. Finally, the expected flash message has also been updated to reflect the new message.
And with that, our tests are all passing!
Terminal
Adding one more test
We should also add a user_session_controller
test to reflect what occurs when a user attempts to log in without first confirming their account.
We’ll add this right after the emits error message with invalid credentials
test.
/test/auth_web/controllers/user_session_controller_test.exs …line 57
We create a non-confirmed user thru the fixture, attempt to log in them in and check for an appropriate response.
A final test run shows 103 passing tests.
Terminal
Summary
That’s it for today, Phx.Gen.Auth
provides a really nice authentication solution. It’s easy to set up and modify; while the included tests provide a nice safety blanket when customizing the functionality.
In part 2 we’ll look at:
- Adding the ability to block a user.
- Swapping out the console notifications with email notifications.
Thanks for reading and I hope you enjoyed the post!