Phoenix Framework has a potent generator for generating authentication for a project. The mix phx.gen.auth
command can generate a complete user authentication system with registration, login, email confirmation, and password reset. But by default, it uses bigserial as the primary key. Using a sequence has drawbacks since it is a predictable counter and anyone can easily know or traverse the service for all available users. For this reason, we sometimes like to use UUID for the primary key.
Today, I’ll go through how to change the default Phoenix behavior of having bigserial
as the primary key of the user table. If you don’t have Phoenix installed, check out the official documentation for how to install it.
Let’s generate a new Phoenix application to test things out:
mix phx.new demoUUIDAuth
The command will generate a new Phoenix application with Postgresql as a database. Now let’s change the DB configs in config/dev.exs
so that Phoenix can connect with our local running database. After that we can generate the LiveView Authentication with the following command:
mix phx.gen.auth Accounts User users --hashing-lib argon2
The --hashing-lib argon2
tells Phoenix which hashing function to use for the password hashing. Available options are, bcrypt, argon2, and pbkdf2. By default, it uses bcrypt. For more information check out the official documentation. After running the command, you’ll be prompted to run the migration and to get the dependencies. Let’s get the dependencies but hold on running the migration because we need to change a few things to have UUID as our user table primary key. Run mix deps.get
to get the dependencies, you’ll see it’ll get the argon2 and comeonin packages.
Since we have the packages, now we can change the schema to tell Ecto that we want to use a different type of primary key for our users
table. Ecto here acts as the data mapper and database adapter. We can start using UUID as the primary key for the users
table by changing the lib/demoUUIDAuth/accounts/user.ex
file like the following:
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @derive {Phoenix.Param, key: :id}
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
timestamps(type: :utc_datetime)
end
By adding the @primary_key
attribute, we’re telling our application that the primary key for the schema will be named id
and it is of type binary
and it’s autogenerated. Ecto v2 guarantees autogeneration of the primary key either by invoking the database function or generation in the adapter. Meaning, either the ID will be generated on the database or it will be generated automatically by the database adapter layer and sent to the database while inserting.
Now, we have to change the users
table definition in the migration file. Open your migration file at priv/repo/migrations/20240224203145_create_users_auth_tables.exs
and change the following:
- create table(:users) do
+ create table(:users, primary_key: false) do
+ add :id, :uuid, primary_key: true
add :email, :citext, null: false
add :hashed_password, :string, null: false
add :confirmed_at, :naive_datetime
timestamps(type: :utc_datetime)
end
This change should have been enough if the user_tokens
table didn’t have a relationship with the users
table. But here the user_tokens
table has a reference of users.id
as user_id
. We also have to change the type of user_tokens.user_id
. Since we’re already on the migration file, let’s change the type of user_id
field by doing the following:
- add :user_id, references(:users, on_delete: :delete_all), null: false
+ add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false
Basically, we’re saying the type on the reference. But this is not enough, since our schema of user_tokens
still doesn’t know the type of user_id
field. We have to do the following changes in the lib/taskchecklist/accounts/user_token.ex
file:
schema "users_tokens" do
field :token, :binary
field :context, :string
field :sent_to, :string
- belongs_to :user, DemoUUIDAuth.Accounts.User
+ belongs_to :user, DemoUUIDAuth.Accounts.User, type: :binary_id
timestamps(updated_at: false)
end
Now, we’re done with our changes and ready to run the mix ecto.migrate
/ mix ecto.setup
command to run our database migrations. After the successful run of the migration we can start the Phoenix app with the following command:
mix phx.server
This will start our Phoenix application in localhost:4000
by default. To register a new user, go to http://localhost:4000/users/register
and register your user. If you look into the logs, you’ll see the id
for the user is UUID. You can also verify this by IEx. Run the following commands:
$ iex -S mix
iex(1)> DemoUUIDAuth.Repo.all(DemoUUIDAuth.Accounts.User)
[debug] QUERY OK source="users" db=5.3ms decode=1.1ms queue=2.6ms idle=1240.8ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:396
[
#DemoUUIDAuth.Accounts.User<
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: "7d715492-3360-4a74-820e-2d9f17d772bd",
email: "[email protected]",
confirmed_at: nil,
inserted_at: ~U[2024-02-24 21:26:12Z],
updated_at: ~U[2024-02-24 21:26:12Z],
...
>
]
iex(2)>
Congratulations! You’ve successfully changed the primary key for the Users table from bigserial to UUID. This might feel like a long process but it’s not that bad considering I’ve included all steps of generating the authentication part. The actual change of bigserial to UUID only takes changes in 3 files. You can checkout the repository of this article on GitHub.
If you have any questions, feel free to comment on this post, or reach out to me via LinkedIn or Email ([email protected]). Thanks for reading!