Modifying phx.gen.auth to use ueber-auth

Published on

Recently I was wanting to add Google oauth account registration and login to my Phoenix LiveView app. I found a few articles, but there were a handful of errors in the tutorial. I adapted the changes to work with my project and figured I would write about it here to help anyone else looking to set up their app as well.

Assumptions

This guide assumes the following:

  • You’ve run mix phx.gen.auth Accounts User users
    • You haven’t made any major changes to the User struct.
  • You’ve set up an application in Google
    • The application has the following scopes in google granted
      • email
      • profile
      • The application has a redirect url safelisted for http://localhost:4000/google/auth/callback
  • You have set the following environment variables
    • GOOGLE_CLIENT_ID
    • GOOGLE_CLIENT_SECRET
  • The top level module is App, a lot of tutorials start with MyApp, just rename this to whatever the top level module is for your app.

Files

mix.exs

First add the deps to your mix.exs. Then run mix deps.get.

  def deps do
    [
      # ...
      {:ueberauth, "~> 0.10.8"},
      {:ueberauth_google, "~> 0.12.1"}
    ]
  end

config/config.exs

Update your config.exs file to have the following configuration.

config :ueberauth, Ueberauth,
  providers: [
    google: {Ueberauth.Strategy.Google, [default_scope: "email profile"]}
  ]

config/runtime.exs

Update your runtime.exs to pull the environment variables to configure the client_id and client_secret.

config :ueberauth, Ueberauth.Strategy.Google.OAuth,
  client_id: System.get_env("GOOGLE_CLIENT_ID"),
  client_secret: System.get_env("GOOGLE_CLIENT_SECRET")

lib/app/accounts.ex

Add a function to the accounts module.

defmodule App.Accounts do
  def register_oauth_user(attrs) do
    %User{}
    |> User.oauth_registration_changeset(attrs)
    |> Repo.insert()
  end
end

lib/app/accounts/user.ex

Update your user.ex schema to support the oauth flow.

defmodule App.Accounts.User do
  schema "users" do
    # ...
    field :is_oauth_user, :boolean, default: false
    # ...
  end

  def oauth_registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email])
    |> validate_required([:email])
    |> validate_email(opts)
    |> put_change(:is_oauth_user, true)
  end
end

lib/app_web/controllers/oauth_controller.ex

Add a controller which uses the new changeset and registration functions.

defmodule AppWeb.OAuthController do
  alias AppWeb.UserAuth
  alias App.Accounts
  use AppWeb, :controller
  require Logger

  plug Ueberauth

  def request(conn, _params) do
    Phoenix.Controller.redirect(conn, to: Ueberauth.Strategy.Helpers.callback_url(conn))
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    email = auth.info.email

    case Accounts.get_user_by_email(email) do
      nil ->
        user_params = %{email: email}

        case Accounts.register_oauth_user(user_params) do
          {:ok, user} ->
            UserAuth.log_in_user(conn, user)

          {:error, changeset} ->
            Logger.error("Failed to create user #{inspect(changeset)}.")

            conn
            |> put_flash(:error, "Failed to create user.")
            |> redirect(to: ~p"/")
        end

      user ->
        UserAuth.log_in_user(conn, user)
    end
  end
end

lib/app_web/router.ex

Wire in the oAuth controller to support support the callback.

scope "/auth", AppWeb do
  pipe_through :browser

  get "/:provider", OAuthController, :request
  get "/:provider/callback", OAuthController, :callback
end

priv/repo/migrations/[timestamp]_add_oauth_user.exs

Run mix ecto.gen.migration add_oauth_user. Then in the file that’s generated replace the contents with the following.

defmodule App.Repo.Migrations.AddOauthUser do
  use Ecto.Migration

  def up do
    alter table(:users) do
      add :is_oauth_user, :boolean, default: false
      modify :hashed_password, :string, null: true
    end
  end

  def down do
    alter table(:users) do
      remove :is_oauth_user
      modify :hashed_password, :string, null: false
    end
  end
end

Testing

Finally run mix ecto.migrate. Once the migrations are completed, run iex -S mix phx.server, then visit https://localhost:4000/google/auth. From there you should be able to go through the full login flow and successfully create a user from the login.

References