An Ueberauth Oauth 2 Walkthrough

Let's set up 3rd party Oauth so our users can log in with Google, Facebook, Twitter, Slack, Github or other services!

Ueberauth is a library that makes it easy and here's a full walk-through of a Phoenix app using it to implement Google login.

All code is shown and explained but due to the number of moving parts, it's a walk-through, not a screencast.

Create a new project

mix phx.new hello
cd hello
mix phx.gen.context Accounts User users avatar email name

After generating the Accounts context and User module, be sure to add a unique index to the users DB migration for email:

create unique_index(:users, [:email])

Install dependencies --(1:49)--

Ueberauth has the Oauth2 library as a dependency, and we'll also need a sub-library for each login strategy we want to implement. mix.exs:

      {:oauth2, "~> 2.0", override: true},
      {:ueberauth, "~> 0.6"},
      {:ueberauth_identity, "~> 0.2.3"},
      {:ueberauth_facebook, "~> 0.8"},
      {:ueberauth_github, "~> 0.7"},
      {:ueberauth_google, "~> 0.8"},
      {:ueberauth_slack, "~> 0.4"},
      {:ueberauth_twitter, "~> 0.3.0"},
      # requried for Oauth2
      poison: "~> 3.1"

Update mix config files --(5:20)--

The next step is to configure the app for Ueberauth. Since there are a lot of options to configure, I prefer to break this into a separate config file. We'll call it config/ueberauth.exs and import it into our config/config.exs right before the

use Mix.Config

config :ueberauth, Ueberauth,
  providers: [
    github: {Ueberauth.Strategy.Github, [default_scope: "user:email"]},
    google: {Ueberauth.Strategy.Google, [default_scope: "email profile"]},
    # any other strategies you want
  ]

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

  # ...

After this is done, set the client_id and client_secret for each provider to whatever Oauth credentials they've given you. You can either hard code these into your config files (and keep production secrets out of source control), or use system environment variables as above.

Add routes to the router --(7:33)--

Create an Auth plug --(9:00)--

The auth module will be very similar to the one we've created for the StatWatch project, this site and the ChitChat server

Create an Auth controller --(14:20)--

For the most part our auth controller follows the standard pattern for a Phoenix app, however, we also need to parse out the data we need from the %Auth{} struct created from the Oauth server's callback:

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    with {:ok, user} <- UserFromAuth.find_or_create(auth),
         {:ok, user} <- Accounts.get_or_create_user(user) do
      conn
      |> put_flash(:info, "Successfully authenticated.")
      |> Auth.put_current_user(user)
      |> redirect(to: "/")
    else
      {:error, reason} ->
        conn
        |> put_flash(:error, reason)
        |> redirect(to: "/")
    end
  end

Accounts.get_or_create_user/1 is a simple function that takes a user struct. If the email field of that struct contains an email for an existing user, it retrieves that user from the database. Otherwise, it uses the entire user struct to create a new user. Either way the user is returned.

UserFromAuth.find_or_create/1 takes an auth struct, parses the user information out of that struct and returns a user struct which can be used to create a new user. This struct gets passed on to Accounts.get_or_create_user/1.

We also need to define a request function for the ouath2 module to use:

  def request(conn, _params) do
    render(conn, "request.html", callback_url: Helpers.callback_url(conn))
  end

The UserFromAuth module --(16:43)--

Next we'll create some utilities for extracting what we need from Auth structs.

defmodule UserFromAuth do
  @moduledoc """
  Retrieve the user information from an auth request
  """
  require Logger
  require Jason

  alias Ueberauth.Auth

  def find_or_create(%Auth{} = auth) do
    {:ok, basic_info(auth)}
  end

  # github does it this way
  defp avatar_from_auth(%{info: %{urls: %{avatar_url: image}}}), do: image

  # facebook does it this way
  defp avatar_from_auth(%{info: %{image: image}}), do: image

  # default case if nothing matches
  defp avatar_from_auth(auth) do
    Logger.warn(auth.provider <> " needs to find an avatar URL!")
    Logger.debug(Jason.encode!(auth))
    nil
  end

  defp basic_info(auth) do
    # Strips complex auth struct to fields in `user.ex`

    %{
      id: auth.uid,
      name: name_from_auth(auth),
      avatar: avatar_from_auth(auth),
      email: email_from_auth(auth)
    }
  end

  defp email_from_auth(%{info: %{email: email}}), do: email

  defp email_from_auth(auth) do
    Logger.warn(auth.provider <> " needs to find an avatar URL!")
    Logger.debug(Jason.encode!(auth))
    nil
  end

  defp name_from_auth(auth) do
    if auth.info.name do
      auth.info.name
    else
      name =
        [auth.info.first_name, auth.info.last_name]
        |> Enum.filter(&(&1 != nil and &1 != ""))

      cond do
        length(name) == 0 -> auth.info.nickname
        true -> Enum.join(name, " ")
      end
    end
  end

end

Using our auth setup

All that's needed to use our Oauth setup now is any page template with a "Sign into Google" button that hooked up to /auth/google and a "Logout" button that sends a DELETE request to /auth/logout. For this app, we'll just add both to the main index page and choose which to show based on whether or not a user is logged in:

  <%= if @current_user do %>
    <h2>Welcome, <%= @current_user.name %>!</h2>
    <img src="<%= @current_user.avatar %>" class="img-circle"/>
    <br>
    <%= button "Logout", to: "/auth/logout", method: :delete, class: "btn btn-danger" %>
  <% else %>
    <a class="btn btn-primary btn-lg" href="/auth/google">
      <i class="fa fa-google"></i>
      Sign in with Google
    </a>
    |
    <a class="btn btn-primary btn-lg" href="/auth/identity">
      <i class="fa fa-user"></i>
      Sign in with Username/Password
    </a>
  <% end %>

(Full code repo available for premium members)

Back to index

No Comments