A deep dive into authentication in Elixir Phoenix with phx.gen.auth
Into the code

To get an overview of the functionality that phx.gen.auth provides, let’s take a look at our router file ( lib/<app_name_web>/router.ex ).

It generated a number of routes and corresponding controller actions. The controllers provide a good separation of concerns and we’ll be looking at how each works in turn below, but, before we move on, it’s worth pointing out that the generator has created some route based authorization in our router file. Note the plugs; require_authenticated_user and redirect_if_user_is_authenticated . These are being imported from the UserAuth module, which provides some utility functions for working with the session and the %Conn{} . We’ll look at UserAuth in more detail below, but it’s worth noting here how certain routes like the registration and sign in routes will redirect if the user is already authenticated. Makes sense, right? Other routes, like changing the user’s settings, require that the user is authenticated. Cool? Alright, moving on.

User Registration

If we look at our our UserRegistrationController module, you can see that it exposes two actions: new/2 and a create/2 .

UserRegistrationController. new/2

The new/2 action is pretty straightforward. It takes a %Conn{} , gets a registration changeset with an empty %User{} struct from the Accounts context, and renders the registration form in the new.html template. Nothing too surprising there.

UserRegistrationController. create/2

The create/2 action takes the %Conn{} and the user_params submitted from the registration form and passes them into Accounts.register_user/1 . Let’s look at Accounts.register_user/1 .

It creates a registration changeset with an empty %User{} struct and the params submitted from the registration form. It then inserts a new user record into the users table in the database. So far so good. Basic Phoenix stuff, right? Before we move on let’s take a look at the User schema file and check how a user is being defined and what that User.registration_changeset/3 function is doing.

We can see that the User schema is being defined with a virtual password field, which means that the password field on the %User{} struct is a derived field and that it is not persisted in the database. The hashed_password , however, is persisted. Aside from that, we have an email field and a confirmed_at field. One last thing to point out on the schema, since the password field is being stored on the %User{} struct, at least in memory, we need to take care not to leak the password accidentally. To mitigate the risk somewhat, the @derive attribute is being used to ensure that the password is not leaked when the Inspect protocol is exercised on the %User{} , eg. when you call IO.inspect(%User{}) .

The registration_changeset/3 function takes a %User{} struct, a map of some attributes, and a keyword list of options. The attributes should look something like this:

%{"email" => "me@example.com", "password" => "some_password"}

The function casts the attributes into a changeset and then pipes the changeset through validate_email/1 and validate_password/2 . One cool thing to notice here is how the opts keyword list is passed to validate_password/2 . The opts keyword list can optionally contain [:hash_password, false] . This prevents validate_password/2 from adding a hashed_password to the changeset. This flexibility is super useful for doing dynamic form validation with LiveView, and negates the need for two separate changeset functions. The phx.gen.auth generator doesn’t actually generate any LiveView code, but it’s some nice forethought. If [:hash_password, false] is not in the opts then the maybe_hash_password/2 will put the hashed password as a change on the changeset. The Bcrypt library’s hash_pwd_salt/1 is being used to handle the hashing with a randomly generated salt. One more thing to note here, is that the confirmed_at field is left untouched. The section below on User Confirmation will deal more with this field, but this field does give you the opportunity to implement custom experiences for both confirmed and unconfirmed users (check out the plug UserAuth.require_authenticated_user/2 for a good place to add this behavior).

Cool, OK. But, we are actually still not done with the create/2 function in the controller. If the call to Accounts.register_user/2 failed for some reason, maybe an email already existed in the database or a password wasn’t long enough, an :error tuple with the changeset will be returned. In which case, the changeset is then passed back to the view so that the errors can be rendered in the template. If the call to Accounts.register_user/2 did succeed, then an :ok tuple with the %User{} struct will be returned. Two things happen then. First, a call is made to Accounts.deliver_user_confirmation_instructions/2 and then the user is actually signed in by calling UserAuth.log_in_user/3 .

Lets’s take a look at Accounts.deliver_user_confirmation_instructions/2 first.

This function takes a %User{} and a function, confirmation_url_fun . The function, in this case, is an anonymous function wrapped around a Routes path function that just returns the path to the confirmation page parametrized with whatever is passed to the anonymous function, which, in this case, will be a token generated by UserToken.build_email_token/2 . 😅That sounded way more complicated than it actually is. Anyways, this function makes use of the UserToken schema for the first time that we’ve seen. Briefly, let’s take a look at how the UserToken schema is defined and how the user confirmation tokens are generated.

The UserToken schema contains a token , a context , and sent_to fields. The token field stores either a raw or a hashed randomly generated binary token. The context field can contain any one of the following strings: ["confirm", "reset_password", "session"] or a dynamic string based on a user’s email, eg. "change:me@example.com" . Lastly, sent_to is an email address. The function build_email_token/2 takes a user and a context, in this case it's "confirm" , and returns a tuple with the first item being a base64 URL encoded token and a %UserToken{} struct containing a hashed version of the token. The hashed version of the token will get persisted in the database in the user_tokens table, whereas the base64 URL encoded version will be included the in confirmation page URL delivered to the user.

If we jump back to Accounts.deliver_user_confirmation_instructions/2 we can see that the newly created %UserToken{} gets inserted into the database and the base64 URL encoded token is being passed to the confirmation_url_fun function, the result from which is being passed to the UserNotifier.deliver_confirmation_instructions/2 . The generated deliver_confirmation_instructions/2 function doesn’t do much at this point. It just logs a message to the console, but the deliver/2 function in UserNotifier could be adapted to call some implementation of Bamboo.

Finally, to actually wrap up the create/2 action in the user registration controller, the user is logged in and a new session is created by calling UserAuth.log_in_user/3 . I’ll cover this in detail below.

User Session

The UserSessionController module handles signing users in and out of the app by creating and destroying sessions. The session controller exposes three actions: new/2 , create/2 , and delete/2 .

UserSessionController. new/2

The new/2 action does nothing except render the sign in form in the new.html template. Note how we’re not using a changeset to create a form, but we are passing an :error_message in the assigns for the %Conn{} .

UserSessionController. create/2

The create/2 action takes the user_params from the sign in form, and attempts to get a user by calling Accounts.get_user_by_email_and_password/2 . If the user is not found, the new.html template is rendered with a generic error message that is intentionally vague. If a user is found, then they are signed in by calling UserAuth.log_in_user/3 . Before we look at that function, let’s look at how we are getting the user in Accounts.get_user_by_email_and_password/2 .

This function looks up a user in the database by the user’s email, and then checks if the user’s password is valid by calling User.valid_password?/2 .

In User.valid_password?/2 , the password is verified by calling Bcrypt.verify_pass/2 . Note that if a user is not found Bcrypt.no_user_verify/0 is called. This is an important defense mechanism in preventing timing attacks that measure the amount of time that an authentication check takes in order to determine if a user exists.

Ok, jumping back to create/2 and into UserAuth.log_in_user/3 .

Let’s look at Account.generate_user_session_token/1 and UserToken.build_session_token/1 .

This is pretty similar to the confirmation tokens that we saw earlier, where a random binary is generated for the token. However, this time the token is not being hashed before being inserted into the database.

Ok, back in UserAuth.log_in_user/1 , a new session is created with the %Conn{} and the session token is added to the session’s assigns. Note that a :live_socket_id is also being assigned to the session’s assigns using the session token so that LiveView sockets can be disconnected when logging out. It’s also worth noting that the token is also being signed into a “remember me” cookie that is set if the user clicks the :remember_me checkbox on the sign in form. Finally, login_in_user/1 redirects the user to either user_return_to or to the signed_in_path/1 . Where is user_return_to coming from?

Remember the plugs that were being used in the Router to protected authenticated routes? One of those was UserAuth.require_authenticated_user/2 .

If the user is not authenticated, it redirects them to the sign in page and adds their current path to the session as :user_return_to . This way we can send the user back to the page that they were trying to access before they were authenticated. Nice. One more thing before we move on. You might be wondering, “Ok, that’s how someone gets logged in, but how do we check if someone is already logged in and that their session token is valid?”. Well, I’m glad you asked. There’s another plug being used in the Router that I didn’t mention earlier. If you look at the browser pipeline in the Router you will see that a fetch_current_user plug was added by the generator. This plug is being used by all of our routes, and is, once again, being imported from the UserAuth module.

The function first attempts to get the user token from the %Conn{} from either the :user_token in the session assigns or from the signed remember me cookie. If it does find a token then the :current_user gets sets as nil on the %Conn{}'s assigns. Otherwise, if it does find a token, it attempts to get a user from the token by calling Accounts.get_user_by_session_token/1 and subsequently, User.verify_session_token_query/1 . Let’s look at those functions.

The query selects from the user_tokens table where the token matches the given token and the context is "session" , and then joins the users table finally returning the user associated with the user token. With the user, fetch_current_user/2 adds the current_user to the %Conn{}'s assigns. That way anything using the %Conn{} later in the pipeline will have access to the current user. Alrighty, that is it for creating a session, let’s look at how we destroy a session with the delete/2 action.

UserSessionController. delete/2

The delete/2 action makes a call to UserAuth.log_out_user/1 to destroy the user’s session.

The current token is retrieved from the session. To clear any session tokens, all rows in the user_tokens table that contain the session token are deleted with the call to Accounts.delete_session_token/1 . After deleting the session tokens, it broadcasts a "disconnect" message for any LiveView socket connections. Finally, it clears the session, deletes the remember me cookie, and redirects the user to the "/" . With this done, the user will be logged out and will need to re-authenticate to visit any routes that require_authenticated_user .

That wraps up the session actions. Hopefully, now you have some understanding of how phx.gen.auth is handling signing a user in and out of the app. Remember back in the Registration section above, where I talked about the confirmation message that was being sent to the user (or logged to the console)? The next section will discuss what happens when a user clicks on the confirmation link in the confirmation message.

User Confirmation

The UserConfirmationController module handles confirming a new user. It exposes three actions: new/2 , create/2 , and confirm/2 .

UserConfirmationController.new/2

The new/2 action renders a page for user’s to resend a confirmation email. The form posts to the create/2 action.

UserConfirmationController.create/2

The create/2 action is pretty much the same thing that we saw in UserRegistrationController.create/2 . A user is looked up from the database by their email address, and a new confirmation message is set just like during registration.

UserConfirmationController.confirm/2

When the user clicks the confirmation link in the confirmation message they will be directed to the path users/confirm/:token which is handled by the UserConfirmationController.confirm/2 action. The confirm/2 action takes the token that is parameterized in the URL and attempts to mark a user as confirmed by calling Accounts.confirm_user/1 . Lets’s take a look at Accounts.confirm_user/1 and the subsequent, UserToken.verify_email_token_query/2 .

The verify_email_token_query is a little bit more complex than what we saw earlier with the verify_session_token_query . With this query, the given token needs to be URL decoded and then hashed before it can be queried against what was actually stored in the database. If a user is not returned when the query gets run then an :error gets returned; however, if a user is found then it’s passed along to the confirm_user_multi/2 transaction which updates the user’s :confirmed_at field, and deletes any confirmation rows from the user_tokens table associated with the user. Back in confirm/2 , regardless of the outcome of the confirmation the user will get redirected to "/" ; however, if the confirmation was successful an appropriate flash message will be shown. If the user was already confirmed, then no message is shown. Finally, if there was an error, an appropriate error flash message is shown.

So thats how user confirmation works. In the next section, we’ll look how phx.gen.auth implements resetting a user’s password for a user that is unable to sign in.

User Password Reset

The UserResetPasswordController module handles, well, resetting the user’s password for users that are not authenticated. It exposes four actions: new/2 , create/2 , edit/2 , and update/2 .

UserResetPasswordController.new/2

This action just renders a form where the user can request a password reset by entering their email. The form posts to the create/2 .

UserResetPasswordController.create/2

This function is very similar to what we saw with UserConfirmationController.create/2 . It attempts to get a user by email and creates a hashed "reset_password" token which gets parametrized in the user_reset_password_url . The user is then redirected to "/" , and is given instructions to check their email in a flash message. As with with the confirmation message, Account.deliver_user_reset_password_instructions/2 currently just logs a message to the console.

UserResetPasswordController.edit/2

The edit/2 action renders the form for changing the user’s password. This form contains a :password_confirmation field whereas the registration didn’t. This is to reduce friction during the registration process. Notice that a user is getting passed to Accounts.change_user_password/2 . Where is that coming from? It’s coming from the get_user_by_reset_password_token/2 plug which the %Conn{} is piped through prior to hitting both the edit/2 and update/2 actions. Dope. The Accounts.get_user_by_reset_password_token/1 function works similar to what we saw above when getting a user by a confirmation token. The given token must be first unwrapped by URL decoding and then hashed before querying the database. If the user isn’t found then the user is redirected to "/" , a helpful flash message is shown, and the %Conn{} is halted so that nothing else in the pipeline is run. However, if a user is found then the change password form is rendered with a changeset generated from the freshly retrieved user.

UserResetPasswordController.update/2

Just like edit/2 , the %Conn{} gets piped through get_user_by_reset_password_token/2 so that user is available in update/2 . The user_params from the password reset form are passed in Accounts.reset_user_password/2 .

Inside of a transaction, reset_user_password/2 updates the user’s password, and also deletes all tokens associated with the user from the user_tokens table to ensure that any session data gets cleared. If there are errors, then a changeset with errors is returned to the user. If the transaction was successful, then the user is redirected the sign in page.

This section looked at resetting the user’s password when they were not authenticated. The section below will look at changing a user’s password and email address when they are authenticated.

User Settings

The UserSettingsController module handles updating the user’s email and passwords for an authenticated user. It exposes three actions: edit/2 , update/2 , confirm_email/2 .

UserSettingsController.edit/2

The edit/2 action is super simple, it just renders two forms on the same page. One form is for changing the user’s email, and one is for changing the user’s password. Note, the changesets for the forms are being set with the assign_email_and_password_changesets plug.

UserSettingsController.update/2

Both the update email and update password forms post to the update/2 action. The function pattern matches on the params to determine what form the params are coming from. Let’s take a look at updating the password first.

The form that posts to the update/2 action contains the user’s current_password and the password change they want to make. These are passed to Accounts.update_user_password/3 along with current_user . This is nearly identical to the Account.reset_user_password/2 that we saw used above to reset passwords. The only difference here is that since the user is logged in, the current_password is verified before the password is changed. Just like when the password was reset above, all of the rows in the user_tokens table associated with the users are deleted to ensure that any session data gets cleared.

Let’s take a look at updating a user’s email now.

The form that posts to the update/2 action contains the user’s current_password and the email change they want to make. The user along with the current_password and email are passed to Accounts.apply_user_email/3 . This function creates a changeset that checks if the email will actually change, validates the new email, and verifies the current_password . Then instead of using the changeset to the update the users table, it applies an :update action on the changeset which returns an updated %User{} struct. This %User{} struct and the user’s previous email is then passed into Accounts.deliver_update_email_instructions/3 . This function behaves similar to other delivery functions that we’ve seen above. Like the other functions, it creates a token that gets parameterized in the URL that gets sent to users (or actually just logged in the console for now). But unlike the functions above, the context for this %UserToken{} becomes "change:#{current_email}" . For example, if my current email was me@example.com and I want to change it to yeezy@example.com , the context would be change:me@example.com and the :sent_to field would be yeezy@example.com . The resulting message is then sent (or logged) to the user’s new address with a confirmation link containing the token. At this point, the user’s email still hasn’t changed in the database. Check out confirm_email/2 to see how it gets updated.

UserSettingsController.confirm_email/2

The confirm_email/2 pulls the token that we discussed above from the params, and passes it to Accounts.update_user_email/2 along with the current_user . Remember, the current_user still has the old email. So in my example from above, it’s still me@example.com . In update_user_email/2 it builds a context string with the "change:#{user.email}" and uses that along with the token to query for the %UserToken{} . Similar to other the email verification queries that we looked at above, the token has to be URL decoded and then hashed before it can be used in the query. Once the %UserToken{} is retrieved. The value from the UserToken.sent_to , yeezy@example.com from the example above, can be passed to a transaction function that finally updates the user in the users table, and deletes any rows from the user_tokens tables for the user and the the "change:#{user.email}" context . If everything works, then the user is redirected to the user settings page. If something goes wrong, then a flash message with a generic error message is displayed and the user is still redirected to the user settings page.

That’s it for user settings.

Wrapping up

That might have got a little deep into the weeds, but the 18 minutes or however long it took you to read this really isn’t that much time. And, I bet that you have a pretty good understanding of phx.gen.auth now; enough, to jump into your own code, make your app’s authentication solution your own, and make it awesome for your users. Ultimately, phx.gen.auth and authentication in Elixir Phoenix really isn’t that scary. It’s mostly just Elixir and Phoenix … which is mostly pretty simple 😏. If you have any questions or comments, let me know in the comments below.
['Cameron Carlyle']
['Functional Programming', 'Phoenix Framework', 'Elixir', 'Authentication', 'Programming']