Setting up Oauth2 in Phoenix

Setting up Oauth2 in Phoenix
Photo by Aaron Burden / Unsplash

First add the dependency to your mix.exs

# /mix.exs

defp deps do
  [
    ...
    {:ueberauth_google, "~> 0.10"}
  ]
end

Then configure Ueberauth within your application

# /config.exs
...
# Ueberauth Config
config :ueberauth, Ueberauth,
  providers: [
    google:
      {Ueberauth.Strategy.Google,
        [default_scope: "email profile openid"]
      }
    ]
    
config :ueberauth, Ueberauth.Strategy.Google.OAuth,
  client_id: <client_id> # generated by google
  client_secret: <client_secret> # generated by google
  redirect_uri: "https://myappsite/auth/google/callback"

The :request route is what the app client will use.

The :callback route is what google will use.

In this instance the callback route will look something like: https://myappsite/auth/google/callback

A short summary of this process looks like this:

A request is made with Ueberauth through your application, to Google. Ueberauth essentially generates a client secret, and to ensure integrity of the data returned, that secret is encrypted and sent along with the request to Google. Google then returns this secret along with user data if the request was successful. The secret is then decoded and if the decoded secret, and the one stored locally are identical, it's safe to assume the returned data is true.

This data is returned to your application through the :callback route.

# /router.ex

# Authentication routes
scope "/auth", ExampleWeb do
  pipe_through :browser
    
  get "/:provider", OauthController, :request
  get "/:provider/callback", OauthController, :callback
end

Next we need the controller that will handle the oauth requests.

defmodule MyAppWeb.OauthController do
  @moduledoc """
  Auth controller responsible for handling Ueberauth responses
  """
  
  use MyAppWeb, :controller
  
  plug Ueberauth
  
  alias MyApp.Accounts.User
  alias MyApp.Accounts
  alias MyAppWeb.UserAuth
  
  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    user_params = %{
      first_name: auth.info.first_name,
      last_name: auth.info.last_name,
      email: auth.info.email,
      avatar_url: auth.info.image,
      locale: auth.extra.raw_info.user["locale"],
      oauth_user_id: auth.extra.raw_info.user["sub"]
    }
    
    changeset = User.changeset(%User{}, user_params)
    
    case Accounts.insert_or_update_oauth_user(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "You are now signed in")
        |> UserAuth.log_in_user(user)
        
      {:error, _reason} ->
        conn
        |> put_flash(:error, "Error signing in")
        |> redirect(to: "/")
    end
  end
  
  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate")
    |> redirect(to: "/")
  end
end

The insert_or_update_oauth_user/1 function.

# /accounts.ex

defmodule MyApp.Accounts do
...
  def insert_or_update_oauth_user(%Ecto.Changeset(changes: %{oauth_user_id: id} = changes}) do
    case Repo.get_by(User, oauth_user_id: id) do
      nil -> %User{oauth_user_id: id}
      user -> user
    end
    |> User.changeset(changes)
    |> Repo.insert_or_update()
  end
...
end

You can see here we are using the unique user id provided by google to authenticate the user. This id is unique to a users' google account and no one will ever have the same one. This is secure because of the process we mentioned before.

Client secret generated, encrypted and sent along with the request to google over an HTTPS connection, user data is returned and validated locally.

To connect it to the front-end, you simply call the :request path. Something like:

<a class="button" href={ Routes.oauth_path(@conn, :request, "google") }>
  Sign in with Google
</a>

https://twitter.com/i_ofin

Follow @i_ofin