José Valim posted this message on Twitter. So here is my experience integrating phx_gen_auth with OAuth for a Phoenix LiveView app.
Some Background
I’m working on (yet another) personal project. It’s a multiplayer browser game written in elixir/phoenix/live_view/surface. If you did not try surface yet, please do. It’s awesome! But more on my game and surface, maybe in a future post.
To start my project, I needed a fast and straightforward way to handle authentication. That was my first contact with phx_gen_auth. After a while, I decided to go with OAuth. Started with Google and planning to extend to other providers later on.
But I wanted to keep the existing DB backed sessions and many other goodies generated by phx_gen_auth. So I used ueberauth on top.
The Demo Example
For my game, I ended up stripping a lot of code that phx_gen_auth initially generated. So for the current article, I created a new phoenix app called (very original) Demo.
For the demo app, I tried to stick as much as possible to the defaults, for both phx_gen_auth and ueberauth. Both packages have very good tutorials, so my implementation is following along with those.
I will not go into details about phx_gen_auth. You can find all you need on the GitHub page and hex.pm. I just want to say that with a single mix command you will get lots of tools and 100+ tests suite out of the box. It is your choice afterward how you want to customize it.
Goal
What the Demo app is about:
- authentication for a LiveView page with Oauth on top of phx_gen_auth
- bonus: store the current_user in the LiveView state
Prerequisites
- a new (or existing) phoenix app generated with the
--live
option - a Google app you can use for OAuth. You can create a new one here: https://console.developers.google.com. You need to configure it to call your dev server, to make the demo work locally
Implementation
Deps and Config
Install the dependencies: phx_gen_auth, ueberauth, ueberauth_google.
# mix.exs
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
{:ueberauth, "~> 0.6"},
{:ueberauth_google, "~> 0.10"}# config.exs
config :ueberauth, Ueberauth,
providers: [
google: {Ueberauth.Strategy.Google, []}
]
Next, we need to store the Google app credentials. For the dev
environment, we can create a dev.secret.exs
and import it in dev.exs
. Do not forget to add the secret config to the .gitignore
!
# dev.secret.exsconfig :ueberauth, Ueberauth.Strategy.Google.OAuth,
client_id: "MY CLIENT ID",
client_secret: "MY CLIENT SECRET"
Generate Auth Files
mix phx.gen.auth Accounts User users
. That’s it! Everything is set up and working. At this point, you can run the tests and see them passing.
Make the phoenix app home page accessible only to authenticated users:
# router.ex
scope "/", DemoWeb do
pipe_through [:browser, :require_authenticated_user]
live "/", PageLive, :index
end
Fetch or create user
For the OAuth case, there is no previous registration step. The user may, or may not exist in the database at the time of sign-in.
# accounts.ex
def fetch_or_create_user(attrs) do
case get_user_by_email(attrs.email) do
%User{} = user ->
{:ok, user} _ ->
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
end
OAuth Controller and routes
You will find references about the auth controller and default routes in the ueberauth
docs. The callback
route is called once the user authenticates with Google.
defmodule DemoWeb.UserOauthController do
use DemoWeb, :controller alias Demo.Accounts
alias DemoWeb.UserAuth plug Ueberauth @rand_pass_length 32 def callback(%{assigns: %{ueberauth_auth: %{info: user_info}}} = conn, %{"provider" => "google"}) do
user_params = %{email: user_info.email, password: random_password()} case Accounts.fetch_or_create_user(user_params) do
{:ok, user} ->
UserAuth.log_in_user(conn, user) _ ->
conn
|> put_flash(:error, "Authentication failed")
|> redirect(to: "/")
end
end def callback(conn, _params) do
conn
|> put_flash(:error, "Authentication failed")
|> redirect(to: "/")
end defp random_password do
:crypto.strong_rand_bytes(@rand_pass_length) |> Base.encode64()
end
end
The only thing I want to mention here is the autogenerated password. It may feel like a workaround. But as I said at the beginning of the post, I would like to stick as much as possible to the default implementation.
Basically, a password is mandatory to create a user. If you don't need a password at all, you can remove the field and the logic around it.
I will come back to this subject in the “Caveats” section below.
# router.ex
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/auth/:provider", UserOauthController, :request
get "/auth/:provider/callback", UserOauthController, :callback
Log-in Template
Add a login link like:
# user_session/new.html.eex
<%= link "LOGIN WITH GOOGLE", to: Routes.user_oauth_path(@conn, :request, "google") %>
Bonus: Current User in the LiveView
The UserAuth.log_in_user/3
called during authentication, puts the user_token
in the session. We can retrieve the session info in the live_view mount/3
callback, and use the user_token
to find the user.
def mount(_params, %{"user_token" => user_token}, socket) do
user = Accounts.get_user_by_session_token(user_token)
{:ok, assign(socket, current_user: user)}
end
Now we have the user in the LiveView state.
Caveats
- this basic example allows the user to log-in with both a password or Google Oauth. The order is not even important. The user may register, then use Google to authenticate. Or the other way: they may sign in with Google, then request a password reset and use the email and the new password to log in. Some websites do not allow this flow. They let the user sign in only with the initial authentication method.
- this basic example can be of course further customized. You can persist the authentication method and the provider. You can then restrict the authentication flow based on that info.
- you can remove the logic you do not use. Eg. the password field and all the templates and controllers related to password reset. They are not needed if you allow the user to authenticate only with OAuth
Resources
- the demo GitHub repo: https://github.com/iacobson/phoenix_demo_app
- the commit that implements OAuth on top of phx_gen_auth: https://github.com/iacobson/phoenix_demo_app/commit/96088c2b5a53c3b4d7a992c3f2385bf83f1a5631
Conclusions
We can create a simple OAuth flow just by following the guides of phx_gen_auth and ueberauth, plus a few lines of code
phx_gen_auth provides the infrastructure and lots of helpers to customize your own authentication solution. Even if you extend it with OAuth, you can reuse all the functions related to user and session management.
As always, any comments and feedback are more than welcome.