phx_gen_auth and OAuth

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

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.

elixir dev | dorian.iacobescu@gmail.com | @iac0bs0n