Issue 013

Implementing Google Authentication in a LiveView Application

Streamline your users login experience in 10 minutes

May 8, 2024 · 8 minute read

mix phx.gen.auth

If you’re an Elixir developer and you’ve created a new Phoenix LiveView application in the last few years then you’ve likely come across the excellent authentication generator that ships out of the box with Phoenix.

For the uninitiated, a brand new Phoenix app can tack on a Postgres-backed, bcrypt-hashsed, session-storing, pre-built authentication system in about 30 seconds right out of the box.

All you have to do is run mix phx.gen.auth and you’ll be asked if you want a LiveView or a DeadView version.

The best part is you get to fully customise the implementation since the generator locally installs everything into your source code, you’re not using someone else’s library or third-party system to manage auth, you’re doing it right in your app. Even if you want to opt for something like Auth0, this is easily the fastest way to add authentication to your new app - for what it’s worth I haven’t seen a faster way to add authentication in any stack.

Why add Google Authentication then?

The truth is, today’s web users have come to expect lightning-fast sign-up journeys, and anything less can raise serious concerns about a website’s legitimacy. Without the familiar comfort of Google’s seamless login experience, you’re essentially inviting users to abandon ship. Let’s face it - friction is a major obstacle that can kill user engagement.

On one of my latest projects, Uini, adding Google Authentication massively improved conversion from visiting users to registered users. Check out Uini if you want to make gathering feedback from your users effortless.

All of this sounds useful and maybe scary, but don't let the intricacies of Google's site deter you! With the right approach, handling OAuth tokens, credentials, and client secrets becomes a seamless process. And I'm here to guide you through it. By harnessing the power of mix phx.gen.auth, you'll discover that integrating authentication with your Phoenix app is remarkably straightforward.

So, let's dive in and tackle this challenge head-on!

First things first - the Google Project

The thing you’ll definitely need here - is a Google account and project.

Head over to the Google Cloud Platform and authenticate through until you can see “API & Services Dashboard”. Run through the basic steps of setting up a project (I’d just choose the same name as the application you’re integrating with), until you have your app setup - this should take a minute or two.

Once your Google Project has been created, you’ll want to navigate to the OAuth consent screen by clicking on the navigation menu item on the left-hand side.

Make your application Public and use the same name as the previous step. I’d leave the scopes to email, profile and opened as a minimum.

Now scroll down and hit save before navigating to the Credentials page. There you’ll want to create credentials, namely OAuth client ID.

From this new shiny page, select Web application give it a name, and you’ll come to the restrictions. Here’s where you tell Google which domains it’s allowed to redirect users back to, to ensure no one is hijacking your applications login.

Add https://localhost:4000 (or whatever port you use for local development) as well as your production environment (and any other) URLs.

Underneath you’ll also see Authorised redirect URIs - this is important and will be the exact path that users are redirected to once authenticated - this is the shiny bit that will bring all that delicious Google data straight to your application so you can store some credentials and authenticate your users.

Hit create.

Now you’ll be shown two values, your client ID and your client secret.

Never show these to anyone but trusted developers working on your application. Do not commit them to git, do not keep them in your source code, do not put them anywhere that can be accessed unsafely.

Okay, the annoying part is done, now you’re ready for some code.

Integrating with ueberauth

We’re gonna use ueberauth/ueberauth: An Elixir Authentication System for Plug-based Web Applications to help take care of all the hard work authenticating with Google.

First we need the core library and the strategy that we want to use. So head into your mix.exs file and add the following to your dependency list:

{:ueberauth_google, "~> 0.10"}

Now pull down your dependencies with mix deps.get and head into your config.exs file to put those earlier secret tokens to good use.

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

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

Again, ensure you store those previous secrets in environment variables on your machines for safety and not in your source code.

Now we need to make a simple controller to ensure all that user information gets sent straight to our database, so new users can have new accounts created or to let in existing users.

defmodule MyAppWeb.GoogleAuthController do
  alias MyAppWeb.UserAuth
  alias MyApp.Accounts
  use MyAppWeb, :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 does not exist, so create a new user
        user_params = %{
          email: email,
          first_name: auth.info.first_name,
          last_name: auth.info.last_name
        }

        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 ->
        # User exists, update session or other details if necessary
        UserAuth.log_in_user(conn, user)
    end
  end
end

This controller takes care of handling the request from Googles callback, checks for an existing user in the database with the email, lets them in or creates an account for them.

You may have noticed the Accounts.register_oauth_user which doesn’t ship out of the box from the generator, so we can simply add that too to my_app/accounts/accounts.ex :

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

We’ll also need to add the changeset function to our user schema too at my_app/accounts/user.ex so let’s add that and add an additional field to track that this user is oauth_based.

 schema "users" do
    ...
    field :is_oauth_user, :boolean, default: false
    ...
 end

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

Alongside creating a migration for this new field, mix phx.gen.auth also requires a hashed password by default, so we need to change that on a database level.

So run mix phx.gen.migration add_oauth_user and ensure you add the following:

  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

Almost done!

Now we need to add our new controller to the router.ex:

scope "/auth", MyAppWeb do
      get "/:provider", GoogleAuthController, :request
      get "/:provider/callback", GoogleAuthController, :callback
    end

And finally add a button to kick start the authentication journey. I like to add a button to core_components.ex like so:

  def google_auth_button(assigns) do
    ~H"""
    <.link href={~p"/auth/google"}>
      <.button>Login with Google</.button>
    </.link>
    """
  end
And that’s it!

If you’ve followed along, you should have an end-to-end sign up flow with Google leveraging the excellent starting point provided by the Phoenix Auth generator.

Session management, database persistence, logging out, and more is all covered.

I hope you found this post useful, subscribe to my Substack below for similar content and follow me on Twitter for more Elixir (and general programming) tips.

If you're building a Phoenix project, I'd also encourage you to take a look at my new open-source component library Bloom to help you out even further.

Enjoyed this content?

Want to learn and master LiveView?

Check out the book I'm writing

The Phoenix LiveView Cookbook
fin

Sign up to my substack to be emailed about new posts