Authentication with Ueberauth, Google Strategy and Guardian

Hey Everyone,

I’ve been fighting this one for more than a day and decided to reach out. I’m sure it’s something trivial but I cannot seem to fix it :see_no_evil:

I’m building a simple admin to manage Organizations and Accounts. The Ueberauth with Google side seems to work fine, I can see a user being created in my database. However, upon the request coming back and hitting the callback, things go awry (as this is where Guardian comes to play, obviously). If y’all wouldn’t mind affording me a few seconds to look at my code?

router.ex

pipeline :browser_auth do
    plug Admin.Auth.Pipeline
  end

  pipeline :browser_ensure_auth do
    plug Guardian.Plug.EnsureAuthenticated
  end

scope "/auth", AdminWeb do
    pipe_through [:browser, :browser_auth]

    get "/login", SessionController, :login
    get "/logout", SessionController, :delete
    get "/:provider", SessionController, :request
    get "/:provider/callback", SessionController, :create
  end

  scope "/", AdminWeb do
    pipe_through [:browser, :browser_auth, :browser_ensure_auth]

    get "/", DashboardController, :index
    resources "/organizations", OrganizationController, only: [:index, :new, :create, :delete]
    resources "/accounts", AccountController
  end

session_controller.ex

defmodule AdminWeb.SessionController do
  use AdminWeb, :controller
  plug Ueberauth

  alias Admin.Repo
  alias Admin.Accounts.Account

  def login(conn, _params) do
    render conn, "login.html"
  end

  def create(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    #map off info struct
    %Ueberauth.Auth.Info{
      email: account_email,
      first_name: account_first_name,
      image: account_photo,
      last_name: account_last_name} = auth.info
    # populate the params, always create as a viewer
    account_params = %{
      token: auth.credentials.token,
      first_name: account_first_name,
      last_name: account_last_name,
      photo: account_photo,
      email: account_email,
      provider: "google",
      roles: ["viewer"]
    }

    changeset = Account.changeset(%Account{}, account_params)

    case insert_or_update_account(changeset) do
      {:ok, account} ->
        Guardian.Plug.current_resource(conn)

        conn
        |> Guardian.Plug.sign_in(account)
        |> put_flash(:info, "Thank you for signing in!")
        |> redirect(to: Routes.dashboard_path(conn, :index))

      {:error, _reason} ->
        conn
        |> put_flash(:error, "Error signing in")
        |> redirect(to: Routes.session_path(conn, :login))
    end
  end

  defp insert_or_update_account(changeset) do
    case Repo.get_by(Account, email: changeset.changes.email) do
      nil ->
        Repo.insert(changeset)
      account ->
        {:ok, account}
    end
  end

  def delete(conn, _params) do
    conn
    |> Guardian.Plug.sign_out()
    |> redirect(to: Routes.session_path(conn, :login))
  end
end

guardian.ex

defmodule Admin.Auth.Guardian do
  use Guardian, otp_app: :admin

  alias Admin.Repo
  alias Admin.Accounts.Account

  def subject_for_token(%Account{} = account, _claims) do
    {:ok, "Account:#{account.id}"}
  end

  def resource_from_claims(claims) do
    IO.inspect claims
    case claims["sub"] do
      "Account:" <> account_id ->
        case Repo.get!(Account, account_id) do
          nil ->
            {:error, :account_not_found}
          account ->
            {:ok, account}
        end
      _ ->
        {:error, :account_not_found}
    end
  end
end

serializer.ex

defmodule Admin.Auth.GuardianSerializer do
  @behaviour Guardian.Serializer

  alias Admin.Repo
  alias Admin.Accounts.Account

  def for_token(account = %Account{}), do: {:ok, "Account:#{account.id}"}
  def for_token(_), do: {:error, "Unknown resource type"}

  def from_token("Account:" <> id), do: {:ok, Repo.get(Account, id)}
  def from_token(_), do: {:error, "Unknown resource type"}
end

pipeline.ex

defmodule Admin.Auth.Pipeline do
  use Guardian.Plug.Pipeline,
    otp_app: :admin,
    module: Admin.Auth.Guardian,
    error_handler: Admin.Auth.ErrorHandler

    @claims %{iss: "Admin"}

  plug Guardian.Plug.VerifySession, claims: @claims
  plug Guardian.Plug.VerifyHeader, claims: @claims, realm: "Bearer"
  plug Guardian.Plug.LoadResource, allow_blank: true
end

error_handler.ex

defmodule Admin.Auth.ErrorHandler do
  import Plug.Conn
  require Logger

  @behaviour Guardian.Plug.ErrorHandler

  @impl Guardian.Plug.ErrorHandler
  def auth_error(conn, {type, reason}, _opts) do
    Logger.warn(Jason.encode!(%{message: to_string(type)}), [])
    Phoenix.Controller.redirect(conn, to: AdminWeb.Router.Helpers.session_path(conn, :login))
  end
end

config.exs

config :guardian, Admin.Auth.Guardian,
  issuer: "Admin",
  verify_issuer: true,
  secret_key: "key",
  serializer: Admin.Auth.GuardianSerializer

I feel like I followed the Getting Started guide pretty closely, but no dice :pensive:

1 Like

Hi,
Welcome to the forum.

Do you get any errors or what do you mean by things go awry also can you post the link of the guide you are following?

Also the more details you provide the better people can offer asisstance

Hey @wolfiton :slight_smile:

After getting the Ueberauth side working, I looked at a few tutorials out there but didn’t really win with any of them. I ended up going to the official one here: https://hexdocs.pm/guardian/tutorial-start.html I figured the only parts I needed to “care” about was setting the current resource, signing in, and signing out.

At the moment, I’m getting this error:

UndefinedFunctionError at GET /auth/google/callback
function Guardian.Plug.sign_in/2 is undefined or private

If you are new to phoenix try to create a simple guardian auth and then add the plugins.

Because of this moment your application doesn’t know what function callback are you referring to also guardian isn’t setup correctly.

Signin has 2 params function and is undefined or private.

Undefined means that the function doesn’t exist and private means that the function can’t communicate with other functions in other files just in the file you created it more specifically only in that module.

One other thing maybe that you misspelled a function and here might be the whole problem.

If you wnat furthjer help the best way is to make a git and share it here,

That way we(community) can all help by looking on what you done until now(the whole code).

Hi @markupguy.
When ever you have a issue with guardian please fell free to post it on GitHub, normally we answer quite quickly, and don’t always check elixirforum :).

I think your problem is that your are calling Guardian.Plug.sign_in instead of Admin.Auth.Guardian.Plug.sign_in. As stated in our documentation

Guardian requires that you create an “Implementation Module”. This module is your applications implementation for a particular type/configuration of token. You do this by use ing Guardian in your module and adding the relevant configuration.

So when you need to use Guardian you need to use your own “Implementation Module”, I know this can be bit confusing at first, but this is the way you are able to configure Guardian to your needs.

Oh thanks @Hanspagh it seems I neglected to add the alias at the top yes.

I’ve also changed my controller’s create function a bit:

case insert_or_update_account(changeset) do
      {:ok, account} ->
        conn
        |> Guardian.Plug.put_current_resource(account)
        |> Guardian.Plug.sign_in(account, %{iss: "Admin"})
        |> put_flash(:info, "Thank you for signing in!")
        |> redirect(to: Routes.dashboard_path(conn, :index))
      {:error, _reason} ->
        conn
        |> put_flash(:error, "Error signing in")
        |> redirect(to: Routes.session_path(conn, :login))
    end

I’ve got a silent error now in my terminal:

** (exit) an exception was raised:
    ** (Plug.Conn.AlreadySentError) the response was already sent
        (plug) lib/plug/conn.ex:764: Plug.Conn.put_resp_header/3

And it looks like I’m still not authenticated, hah. I will keep fighting.

Thanks so much for the help!

Normally the response was already sent means that you tried to send a http response multiple times