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)
1 Comment
UndefinedFunctionError at GET /auth/identity function HelloWeb.AuthView.render/2 is undefined (module HelloWeb.AuthView is not available) when tried to sign in using username and password.