BinaryWebPark

How To Do Google OAuth Authentication With Phoenix and Elixir

October 10, 2017

How To Do Google OAuth Authentication With Phoenix and Elixir

A while back, I had to enable a login system using the OAuth via Google credentials in a Phoenix application. Quite a bit of this came from this nice article and this code repository by scrogson.

OAuth logo

But as with all things in technology, there were a few bits that seemed to be out of date or were missing. This article fills in those gaps with a complete working solution so that you don’t have to Google around anymore.

Step 1 – Install the oauth2 hex package

If you don’t know how to install Elixir, I covered it in this article.

Configure your mix.exs file as follows:

# Type `mix help compile.app` for more information.
  def application do
    [mod: {MyApp, []},
     applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :timex, :oauth2]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.2.0"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:oauth2, "~> 0.9"}]
  end

Step 2 – Create the authorization controller

You have to create an authorization controller with one slight change.

In the get_user! method user of your authorization controller you’ll have to use this line of code:

%{body: user, status_code: status} = OAuth2.Client.get!(client, "https://www.googleapis.com/plus/v1/people/me/openIdConnect")

Here is the controller code:

defmodule MyApp.AuthController do
  use MyApp.Web, :controller

  @doc """
  This action is reached via `/auth/:provider` and redirects to the OAuth2 provider
  based on the chosen strategy.
  """
  def index(conn, %{"provider" => provider}) do
    redirect conn, external: authorize_url!(provider)
  end

  def delete(conn, _params) do
    conn
    |> put_flash(:info, "You have been logged out!")
    |> configure_session(drop: true)
    |> redirect(to: "/")
  end

  @doc """
  This action is reached via `/auth/:provider/callback` is the the callback URL that
  the OAuth2 provider will redirect the user back to with a `code` that will
  be used to request an access token. The access token will then be used to
  access protected resources on behalf of the user.
  """
  def callback(conn, %{"provider" => provider, "code" => code}) do
    client = get_token!(provider, code)

    user = get_user!(provider, client)

    conn
    |> put_session(:current_user, user)
    |> put_session(:access_token, client.token.access_token)
    |> redirect(to: "/")
  end

  defp authorize_url!("google"),   do: Google.authorize_url!(scope: "https://www.googleapis.com/auth/userinfo.email")
  defp authorize_url!(_), do: raise "No matching provider available"

  defp get_token!("google", code),   do: Google.get_token!(code: code)
  defp get_token!(_, _), do: raise "No matching provider available"

  defp get_user!("google", client) do
    %{body: user, status_code: status} = OAuth2.Client.get!(client, "https://www.googleapis.com/plus/v1/people/me/openIdConnect")
    %{email: user["email"], domain: user["hd"], email_verified: user["email_verified"], avatar: user["picture"]}
  end
end

Now the above controller code is pretty much straight from scrogson’s repository with a few minor changes. Namely, I altered the getuser!_ method to parse information from the Google OAuth handshake a bit differently. Also, as of this writing, I noticed the getuser!_ method in scrogson’s repository didn’t work out of the box due to a different data structure I encountered.

I don’t know if Google changed the response it sends back or what happened.

Step 3 – Create a Google OAuth strategy module

One thing I had to do differently from the scrogson repository is to change the signature call of OAuth2.client.gettoken!_.

I had to use the following line of code in the get_token! method:

OAuth2.Client.get_token!(client(), Keyword.merge(params, client_secret: client().client_secret))

defmodule Google do
  @moduledoc """
  An OAuth2 strategy for Google.
  """
  use OAuth2.Strategy

  alias OAuth2.Strategy.AuthCode

  defp config do
    [strategy: Google,
     site: "https://accounts.google.com",
     authorize_url: "/o/oauth2/auth",
     token_url: "/o/oauth2/token"]
  end

  # Public API

  def client do
    Application.get_env(:competitive_networks, Google)
    |> Keyword.merge(config())
    |> OAuth2.Client.new()
  end

  def authorize_url!(params \\ []) do
    OAuth2.Client.authorize_url!(client(), params)
  end

  def get_token!(params \\ [], headers \\ []) do
    OAuth2.Client.get_token!(client(), Keyword.merge(params, client_secret: client().client_secret))
  end

  # Strategy Callbacks

  def authorize_url(client, params) do
    AuthCode.authorize_url(client, params)
  end

  def get_token(client, params, headers) do
    client
    |> put_header("Accept", "application/json")
    |> AuthCode.get_token(params, headers)
  end
end

Step 4 – Enable The Google Plus API

You’ll need to enable the Google Plus API from the Google developer’s console.

Enable Google Plus API

Step 5 – Create your credentials and register a redirect url

First, click the create credentials button.

Your new google credentials screenshot

Next, you’ll see your client id and client secret. You’ll also need to put in a redirect URL for the OAuth callback.

Step 6 – Configure your app with the client id and client secret

In config.exs, you’ll set up a client id, client secret and redirect url for your oauth callback.

config :my_app, Google,
  client_id: System.get_env("CLIENT_ID"),
  client_secret: System.get_env("CLIENT_SECRET"),
  redirect_uri: System.get_env("REDIRECT_URI")

Step 7 – Set up your routes in router.ex

scope "/auth", MyApp do
    pipe_through :browser

    get "/:provider", AuthController, :index
    get "/:provider/callback", AuthController, :callback
    delete "/logout", AuthController, :delete
  end

Step 8 – Setup your views

Create a template in web/templates/layout in signinsign_out.html.eex.

<%= if @current_user do %>
  <h2>Welcome, <%= @current_user.email %>!</h2>
  <img src="<%= @current_user.avatar %>" class="img-circle"/>
  <%= button "Logout", to: auth_path(@conn, :delete), method: :delete, class: "btn btn-danger" %>
<% else %>
  <br/>
  <br/>
  <a class="btn btn-primary btn-lg" href="<%= auth_path @conn, :index, "google" %>">
    <i class="fa fa-google"></i>
    Sign in with Google
  </a>
<% end %>

Now add signinsign_out.html to the template in web/templates/layout/app.html.eex.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
    <title>MyApp</title>
  </head>

  <body>
    <div class="jumbotron">
      <h2>Welcome to Your App</h2>
    </div>
    <div class="container">
      <header class="header">
        <nav role="navigation">
          <ul class="nav nav-pills pull-right">
            <%= render "sign_in_sign_out.html", conn: @conn, current_user: @current_user %><br><br>
          </ul>
        </nav>
      </header>
    </div>
    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-md-8">
          <main role="main">
            <%= render @view_module, @view_template, assigns %>
          </main>
        </div>
      </div>
    </div>
  </body>
</html>

Step 9 – Add a user model

defmodule MyApp.User do
  use MyApp.Web, :model

  def name(user), do: user["name"]
  def email(user), do: user["email"]
end

Step 10 – Add a .env file for local testing

export GOOGLE_CLIENT_ID=xxx
export GOOGLE_CLIENT_SECRET=xxx
export GOOGLE_REDIRECT_URI=xxx

Step 11 – Try it out!

  1. Type source .env at the command line.
  2. Run mix phoenix.server and navigate to the home page.

When you click on the login button, behind the scenes you’ll get a response back from Google that looks something like the following.

%OAuth2.Response{body: %{"email" => "bruce@fakedomain.com", "email_verified" => "true", "family_name" => "", "given_name" => "", "hd" => "binarywebpark.com", "kind" => "plus#personOpenIdConnect", "name" => "", "picture" => "https://lh5.googleusercontent.com/-bGZdkE34lMA/BBBAAAAAI/AAAAAAAAABo/VlNNKYfRHI8/photo.jpg?sz=50", "sub" => "113114266206295700617"}, headers: [{"expires", "Sun, 05 Feb 2017 05:04:35 GMT"}, {"date", "Sun, 05 Feb 2017 05:04:35 GMT"}, {"cache-control", "private, max-age=0, must-revalidate, no-transform"}, {"etag", "\"FT7X6cYw9BSnPtIywEFNNGVVdio/iIt_m-6wkBxd2q2IbOAHtsO7YPc\""}, {"vary", "Origin"}, {"vary", "X-Origin"}, {"content-type", "application/json; charset=UTF-8"}, {"x-content-type-options", "nosniff"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"content-length", "330"}, {"server", "GSE"}, {"alt-svc", "quic=\":443\"; ma=2592000; v=\"35,34\""}], status_code: 200}

Summary & Resources

Once again, here are the links to the article(s) and example repositories I described.

  1. Reference article on Google OAuth in Phoenix
  2. Example code repository by scrogson.