Phx.Gen.Auth is a great authentication solution when building out Phoenix applications. I’ve previously gone over some options for customizing the functionality and workflow around Phx.Gen.Auth
. Today we’ll look at how we can restrict user registrations via a registration token.
Updated 2021-11-04:
A previous version of this post used a UUID for the registration token. A reader pointed out this opens the registration tokens up to timing attacks. Essentially someone can systematically test the possible UUID values, track the resulting response times of the application and in this way discover registration token values. I have to admit I hadn’t considered timing attacks when deciding to go with UUIDs. I hadn’t really thought of the registration tokens in the same terms as say a password reset token. But really, this is faulty thinking, depending on the business logic of your application a registration token could be just as important as a password reset or confirmation token.
For those interested in some further details around timing attacks and UUIDs, I found the following articles very interesting:
- https://codahale.com/a-lesson-in-timing-attacks/
- https://littlemaninmyhead.wordpress.com/2015/11/22/cautionary-note-uuids-should-generally-not-be-used-for-authentication-tokens/
- https://versprite.com/blog/universally-unique-identifiers/
- https://neilmadden.blog/2018/08/30/moving-away-from-uuids/
I think the security considerations section of the UUID specification sums things up pretty succinctly: “Do not assume that UUIDs are hard to guess; they should not be used as security capabilities”.
So instead of a UUID, we’ll use the same technique as Phx.Gen.Auth
. This means storing a hashed version of the registration token in the database and supplying a url encoded, non-hashed version of the token to the user. When the user provides the non-hashed token back to our application we hash the value and compare it with the hashed token in the database to determine whether it is valid.
Okay… on to our regularily scheduled programming… we’ll create a fairly flexible solution:
- Registration will require a registration token, which will be included in the URL for the registration page.
- The token itself will come in a few different flavors:
- The token can be
generic
and anyone who has the token / URL can register with it. - The token can be scoped to an email address, in which case it will only work when registering with the scoped email address.
- An optional
generated_by
field is available which associates a token to an existing user. This could be useful if we want to allow existing users to generate tokens and invite other users to the application. Having thegenerated_by
field provides a way to trace back thru the invites to see who has invited a particular user or group of users.
- The token can be
- We’ll also create a version where we store the url encoded, non-hashed token string in the database and a version where we don’t store the token string in the database. The advantage of storing the token string is we have a bit more flexibility in terms of how we distribute and manage the tokens; they don’t need to be distributed at generation time. The disadvantage is we now have plain text token values in our DB, if someone gains read access to the DB they can use the token strings to register.
Note: I’m not a fan of “flexible” solutions unless there is a use case to support the flexibility. Adding extra scenarios because “you might need them” is a great way to add extra code, effort and bloat. If your requirements are more locked down I’d suggest a targeted approach. For instance if I only needed email scoped tokens, I would not implement non-email scoped tokens.
Anyway, enough preamble, let’s get down to some code!
Getting started
First off let’s create a new Phoenix project. I’m using the latest and greatest version of Phoenix, Phoenix 1.6. If you are on an older version of Phoenix, you’ll need to install Phx.Gen.Auth
as a dependency via your mix file as it was only merged into Phoenix as of version 1.6. There will also be some other minor differences you’ll encounter especially around templates as 1.6 introduced heex
templates.
Terminal
Note: add a --live
flag to the above if using a Phoenix version older than 1.6.
Select yes when asked to install dependencies.
Now we can change into the directory where the application was created and build our database.
Terminal
Terminal
Next we’ll generate the authentication code.
Terminal
We’ll follow the instructions from the mix
output:
Terminal
Finally let’s ensure all our tests are running out of the gate.
Terminal
Great! Everything is looking good, we’re ready to make our changes to the registration workflow. We’ll start off with the backend changes.
Updating the backend to restrict registrations
Starting with the data layer
Since we’ll be requiring a token in order to register, we need somewhere to store registration tokens. There is already a user_tokens
table that gets created as part of phx.gen.auth
.
We could look to re-use and augment this table to also work with registration tokens. I think it’s going to make more sense and be easier to create a specific table for the registration tokens however… so we’ll need a migration for that!
Terminal
The contents of the migration will be as follows.
/priv/repo/migrations/time_stamp
_create_registration_tokens.exs
Pretty straight forward. Just like the user_tokens
table we use a binary
column for the token. We’ll populate this column with a hashed version of the registration token. We’ve also added a token_string
field which we’ll use to hold a base 64 encoded version of the non-hashed token. This is the representation of the token that will be provided to the user in order for them to register. Depending on how you supply tokens to your users this column might not be necessary. For instance if you send an email or SMS message with a registration link you might not need to store this column… however it does mean if the user misplaces the email or text, a new token would need to be generated for them; the token_string
value can’t be regenerated from the token
field.
As mentionned earlier, the advantage of storing the token_string
is we have a bit more flexibility in terms of how we distribute and manage the tokens. The disadvantage is we now have plain text token string values in our DB, if someone gains read access to the DB they can use the token strings to register. We’ll look at how to alter the implementation to not store the token strings in part 2.
In addition to the above two fields, we’ve created a used_by_user_id
column and a generated_by_user_id
column which have references back to the user table. These will contain references to the user who generated the token (if applicable) and the user who registered with the token. Then we have a scoped_to_email
column for the scenario where we want to restrict the token to a particular email address. Everything is nullable
other than the token and token string columns.
Finally we have a unique index on the used_by_user_id
column as a user’s registration should only ever be associated with a single token.
Let’s run the migration.
Terminal
And now we can create the schema file.
Terminal
/lib/reg_tokens/accounts/registration_token.ex
Simple, we’re just representing the database table as an Ecto Schema.
That’s it for the data layer… let’s start building out the context functions.
The context functions
We need two things out of our context; the ability to generate registration tokens and the ability to register a user with a token. Since the second of these two options requires a token, let’s start with figuring out how to generate that token.
Token generation
This is pretty easy. We’ll add a new alias and function to the existing accounts.ex
context module.
/lib/reg_tokens/accounts.ex
We’ve added the RegistrationToken
schema to our list of aliases… and now for the generation function:
/lib/reg_tokens/accounts.ex
Simple! We’re passing in our optional values (the generated_by
and scoped_to_email
fields) as a keyword list. This works great as Keyword.get returns nil
when the key is missing from the keyword list… which is exactly what we want to insert when these fields are not supplied.
We generate the token via a RegistrationToken.build_hashed_token
function we’ll need to implement. It returns both the url encoded non hashed string version of the token and the hashed token.
We create a map from the tokens and the opts
parameters:
… which we pipe into a changeset function (RegistrationToken.create_changeset
), the result of which gets piped into Repo.insert
.
All standard stuff, we need to add the build_hashed_token
and create_changeset
functions referred to above in registration_token.ex
.
/lib/reg_tokens/accounts/registration_token.ex
We’ve added a new import for Ecto.Changeset
. The build_hashed_token
function doesn’t have too much going on:
Essentially the same as the build_hashed_token
function in user_tokens.ex
… there is some duplication we could look to refactor out between these 2 functions, but I’m not sure it’s worth it in this particular case.
As far as the changeset function goes…
We cast the four fields we expect in the attrs
map and then add a foreign key check for the generated_by_user_id
field as we want to return an error if the user id for the generating user does not exist.
Let’s give it a spin, we’ll fire up iex
.
Terminal
… and generate some tokens.
Terminal
Terminal
If we want to create a token with a generated_by
value, we’ll first need to create a user.
Terminal
Then we can create a token with a generated_by
value set to the id of the above user.
Terminal
Great, everything seems to be working and if we look at our data with something like TablePlus we’ll see the 3 tokens we’ve created.
iex
is a super convenient way to test drive functionality while building things out but we’ll definitely want to write some tests… let’s tackle that next.
Token generation tests
Just like the implementation, the tests will be pretty straight forward.
First thing, we’ll need a new alias in accounts_test.exs
.
/test/reg_tokens/accounts_test.exs
Now we’ll create a new describe
block for the token generation tests.
/test/reg_tokens/accounts_test.exs
Pretty self-explanatory, we’re testing the various token options and ensuring the token is created with the expected values… and we have a test to make sure the generated_by
foreign key constraint is acting as expected.
Let’s make sure everything is passing.
Terminal
Nice!
Onto the registration function.
User registration
We have the token generation part of our registration workflow done and dusted, now we need to register a user with a token. We’ll create a register_user_with_token/2
function in accounts.ex
. We’ll build this function up bit by bit, starting with the top level function and filling in the supporting pieces as we proceed thru the explanation.
/lib/reg_tokens/accounts.ex
The function above maps out the basic workflow of the registration functionality. We retrieve the registration token and pipe the result of the retrieval into maybe_register_user_with_token
; which is the function that performs the actual registration.
Let’s implement the token retrieval:
/lib/reg_tokens/accounts.ex
We’re decoding and hashing the token which we then use when building the retrieval query (get_registration_token_query
). In the case where the decode fails or the token isn’t a string (i.e. the when is_binary(token)
clause fails) we return nil
.
The query function in registration_token.ex
looks like :
/lib/reg_tokens/accounts/registration_token.ex
This is pretty simple, we are querying for a token that matches the hashed token passed to the function. Then we check the token has not expired and hasn’t been used. I’ve settled on an expiry of 30 days but depending on your use case you might want to tighten up the expiry period.
We also need to add a Ecto.Query
import at the top of the file.
/lib/reg_tokens/accounts/registration_token.ex
So that takes care of token retrieval, the only thing left is maybe_register_user_with_token/2
. We’ll use some pattern matching to handle the various scenarios that can arise when piping the retrieved token into maybe_register_user_with_token
. Our function signatures will look like:
Let’s get to the implementation, we’ll tackle the above cases one by one.
/lib/reg_tokens/accounts.ex
The first case is simple. If we don’t retrieve a token from get_registration_token
we know either the token doesn’t exist, is expired, or has already been used. We pattern match on nil
as the token’s value and send back an error changeset via a new function we’ll create, invalid_token_response
.
/lib/reg_tokens/accounts.ex
I’ve chosen to return a generic invalid token message as part of the changeset, but depending on your front end requirements you might want to return specific messages around specific failures or even return specific error tuples. For instance instead of returning a changeset, you could return something like {:error, :expired_token}
. This would be useful if you want to provide more detailed information around what went wrong to the caller of the registration function.
The next case is a non-email scoped token.
/lib/reg_tokens/accounts.ex
Again we use pattern matching to determine that we have retrieved a token… but it is a non-email scoped token (i.e. via %RegistrationToken{scoped_to_email: nil}
). We then use a transaction
to register the user and consume the token. If for some reason either of these operations fail we perform a rollback. register_user/1
already exists, we’re just re-using the existing registration function from Phx.Gen.Auth
, we need to write consume_registration_token
however.
/lib/reg_tokens/accounts.ex
Simple! Just a repo update with some help from a new changeset function we’ll create in registration_token.ex
.
/lib/reg_tokens/accounts/registration_token.ex
Nothing complicated, we set the used_by_user_id
value to the id of the created user and perform a unique constraint check.
That’s it for non email scoped tokens, the final case to handle is tokens scoped to an email.
/lib/reg_tokens/accounts.ex
Very similar to the non-email scoped version of the function. The only difference being we have a surronding if
statement that ensures the registering user’s email is the same as the email in the token. The Repo.transaction
blocks are the same in both places, I thought about creating another sub function to remove the duplication but felt it read better having the transaction blocks inline.
Trying it out
Let’s give our code a spin in iex
.
Terminal
First we’ll try an invalid token string.
Terminal
The output looks as expected.
Next let’s try a valid token. First we’ll create a token.
Terminal
And then register.
Terminal
Sweet! Looks like things are working, feel free to run thru some other scenarios with iex
such as mis-matching email addresses… all of which we’ll be tackling in our tests.
Adding some tests
Test time! The first thing we’re going to do is update the existing register_user/1
tests to run thru register_user_with_token/2
. When we get around to the front-end changes we’ll make register_user
a private function. In anticipation of that and to ensure we’re covering off the existing functionality around registration, we want to update the existing tests to run thru our new registration function.
To accomplish this, we need to:
- Add a
setup
block. - Update the test signatures.
- Replace the
register_user
function calls withregister_user_with_token
. - Update the title of the describe block, replacing
describe register_user/1
withdescribe register_user_with_token/2
.
Finally we won’t bother testing the happy path scenario as we’ll do so in subsequent tests.
To achieve the above, replace the current register_user/1
describe block with the following:
/test/reg_tokens/accounts_test.exs
We’re making use of a new fixture function during the test setup (registration_token_fixture
), so we need to add this to accounts_fixtures.ex
.
/test/support/fixtures/accounts_fixtures.ex
Let’s make sure the account tests are still passing.
Terminal
Nice! Next we’ll add some tests for non-email scoped tokens.
/test/reg_tokens/accounts_test.exs
I think these are all pretty self-explanatory. The first test validates the happy path, the remaining tests check the failure scenarios: a token which is not a string; a token which has expired; and a token which has already been used.
Let’s try ‘em out.
Terminal
Fantastic! Now let’s create a similar describe block for email scoped tokens.
/test/reg_tokens/accounts_test.exs
This is largely the same as the previous describe block, this time we’ve just added a test for the scenario where the registering email address does not match the email address the token is scoped to.
Let’s run thru the full test suite.
Terminal
Awesome, all good! This closes out the backend changes required for the new registration workflow.
Summary
I was hoping to get everything done in a single go but this post is starting to get long… so we’ll handle the front-end changes in an upcoming post.
Todays code is available on GitHub, you can grab it via:
Terminal
Terminal
You should now see the following git history.
Terminal
Thanks for reading and I hope you enjoyed today’s post!