Azer Koçulu Nov 09, 2024

Adding Google OAuth to Phoenix

Recently added Google authentication to Mitte. Writing this down as I don't want to remember all the steps next time.

Here's how to add Google OAuth to a Phoenix app, step by step.

Step 1. Setting up OAuth Credentials

First, we need OAuth2 credentials. Grab your Client ID and Client Secret by following these steps:

Step 2. Configuration

Add OAuth dependencies to mix.exs:

{:ueberauth, "~> 0.10"},
{:ueberauth_google, "~> 0.10"}

Install new dependencies:

$ mix deps.get

Configure OAuth settings in config/config.exs:

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

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

Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET into your prod / dev environment. You might want to move the configuration to config/runtime.exs if you like to load .env file first;

# Install if missing from https://hex.pm/packages/dotenv_parser
if config_env() == :dev do
  DotenvParser.load_file(".env")
end

Step 3. Database & Schema

Now let's create a migration add OAuth fields to our users table:

$ mix ecto.gen.migration add_oauth_fields_to_users

We'll store user's name, avatar URL, OAuth provider name, and access token. Also make the password field optional as OAuth users won't have passwords:

defmodule MyApp.Repo.Migrations.AddOAuthFieldsToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :name, :string
      add :avatar_url, :string
      add :provider, :string
      add :token, :string
      modify :hashed_password, :string, null: true  # Allow OAuth users without password
    end
  end
end

Run the migration:

$ mix ecto.migrate

And update the User schema in lib/myapp/accounts/user.ex:

schema "users" do
  field :email, :string
  field :password, :string, virtual: true, redact: true
  field :hashed_password, :string, redact: true
  field :confirmed_at, :naive_datetime
  field :name, :string
  field :provider, :string
  field :token, :string
  field :avatar_url, :string

  timestamps()
end

Add OAuth changeset - this handles user creation and updates when they sign in with Google:

def oauth_changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :name, :provider, :token, :avatar_url])
  |> validate_required([:email, :provider])
  |> validate_email([])
  |> unique_constraint(:email)
  |> put_change(:confirmed_at, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
end

Step 4. Routing & UI

Add routes in lib/app_web/router.ex:

scope "/auth", MyAppWeb do
  pipe_through [:browser, :redirect_if_user_is_authenticated]

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

Create the auth controller in lib/myapp_web/controllers/auth_controller.ex:

defmodule MyAppWeb.AuthController do
  use MyAppWeb, :controller
  plug Ueberauth

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    user_params = %{
      email: auth.info.email,
      name: auth.info.name,
      provider: "google",
      avatar_url: auth.info.image,
      token: auth.credentials.token
    }

    case Accounts.get_or_create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Welcome!")
        |> UserAuth.log_in_user(user)

      {:error, _reason} ->
        conn
        |> put_flash(:error, "Authentication failed")
        |> redirect(to: ~p"/login")
    end
  end
end

Finally, we can now add the Google sign in button to lib/myapp_web/controllers/user_session_html/new.html.heex;

  <a
    href="/auth/google"
    class="flex justify-center relative text-center no-underline bg-[#1e2229] text-[#fff] transition mt-[10px] px-[10px] py-[15px] border-[0] w-full rounded-[7px] text-[16px] border-[1px] border-[rgba(0,0,0,.1)] shadow font-semibold cursor-pointer hover:-translate-y-[2px]"
  >
    <div class="w-[20px] h-[20px] mr-[12px]">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 533.5 544.3"
        width="100%"
        height="100%"
        style="display:inline-flex; align-items:center;"
      >
        <path
          d="M533.5 278.4c0-18.5-1.5-37.1-4.7-55.3H272.1v104.8h147c-6.1 33.8-25.7 63.7-54.4 82.7v68h87.7c51.5-47.4 81.1-117.4 81.1-200.2z"
          fill="#fff"
        >
        </path>
        <path
          d="M272.1 544.3c73.4 0 135.3-24.1 180.4-65.7l-87.7-68c-24.4 16.6-55.9 26-92.6 26-71 0-131.2-47.9-152.8-112.3H28.9v70.1c46.2 91.9 140.3 149.9 243.2 149.9z"
          fill="#fff"
        >
        </path>
        <path
          d="M119.3 324.3c-11.4-33.8-11.4-70.4 0-104.2V150H28.9c-38.6 76.9-38.6 167.5 0 244.4l90.4-70.1z"
          fill="#fff"
        >
        </path>
        <path
          d="M272.1 107.7c38.8-.6 76.3 14 104.4 40.8l77.7-77.7C405 24.6 339.7-.8 272.1 0 169.2 0 75.1 58 28.9 150l90.4 70.1c21.5-64.5 81.8-112.4 152.8-112.4z"
          fill="#fff"
        >
        </path>
      </svg>
    </div>
    <label class="cursor-pointer">Continue with Google</label>
  </a>

Step 5. Email Notification

Let's also notify users when they sign in with Google for the first time. First, add new notification method to lib/myapp/accounts/user_notifier.ex:

def deliver_oauth_welcome_message(user) do
  html_body = """
  <div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
    <h1 style="font-size: 24px; color: #1a1a1a;">Welcome to MyApp! ✨</h1>
    <p style="color: #333; line-height: 1.5;">Thanks for signing up with Google. Your account is verified and ready to go.</p>
    <p style="margin: 25px 0;">
      <a href="https://myapp.com" style="background: #1a1a1a; color: white; padding: 10px 20px; border-radius: 5px; text-decoration: none;">Start Using MyApp</a>
    </p>
    <p style="color: #666; font-size: 14px;">Or visit: https://myapp.com</p>
    <p style="color: #333; margin-top: 30px;">Best,<br>MyApp Team</p>
  </div>
  """

  text_body = """
  Welcome to MyApp! ✨

  Thanks for signing up with Google. Your account is verified and ready to go.

  Start using MyApp at: https://myapp.com

  Best,
  MyApp Team
  """

  deliver(user.email, "Welcome to MyApp! ✨", html_body, text_body)
end

Then update the auth controller (lib/myapp_web/controllers/auth_controller.ex) to send email when user signs up:

  case Accounts.get_or_create_user(user_params) do
    {:ok, user} ->

      # Send email if the user was just created
      if just_created?(user) do
        Accounts.UserNotifier.deliver_oauth_welcome_message(user)
      end

      ...
  end

Don't forget adding a helper function to check if the user was just created:

defp just_created?(user) do
  case user.inserted_at do
    nil -> true
    created_at ->
      NaiveDateTime.diff(NaiveDateTime.utc_now(), created_at) < 5  # Within last 5 seconds
  end
end

That's all needed for Google authentication. The setup above handles both new and returning users, stores their name and profile picture, and automatically confirms their email.

Back