Phoenix with Ueberauth and Guardian between Vue and GenServer

Robert Guiscard
5 min readApr 25, 2019

--

Modern web application has Vue/Javascript on frontend and probably GenServer on the backend. To have proper authentication is not straight forward in Phoenix. Here is my implementation as a reference.

First, user still use regular Phoenix view to sign up or sign in. Once it is authenticated, it will be redirected to pages with Vue as frontend, talking to Phoenix through channel. On the other hand, there is a GenServer per user in backend to handle some long-lasting tasks. In this case, GenServer will be a XMPP client.

These articles provide details to set up regular Phoenix authentication with Guardian and Ueberauth easily. At this point, you can decide which pages are open to public and which ones requires authentication.

My config/config.exs looks like this:

# Ueberauth and identity
config :ueberauth, Ueberauth,
providers: [
identity: {Ueberauth.Strategy.Identity, [
callback_methods: ["POST"],
param_nesting: "user",
uid_field: :username,
nickname_field: :username,
request_path: "/sessions/new",
callback_path: "/sessions/identity/callback",
]}
]

Then routes in lib/xapp_web/router.ex like this

resources "/sessions", SessionController, only: [:new, :delete]
post "/sessions/identity/callback", SessionController, :identity_callback

Guardian.Plug provides ways to sign in and out of user like this:

App.Guardian.Plug.sign_in(conn, user)
App.Guardian.Plug.sign_out(conn)

Once signed in, current_user can be obtained, usually in a plug like this:

# lib/app_web/plugs/current_user.exdefmodule AppWeb.Plug.CurrentUser do
import Plug.Conn
import Guardian.Plug
def init(_params) do
end
def call(conn, _params) do
user = current_resource(conn)
assign(conn, :current_user, user)
end
end

Then this plug can be added to pages which needs it. Current user can be accessed via @conn.assigns[:current_user] in views.

For pages with vue, which talk to Phoenix through socket and channel, you need a token to validate the vue client. Phoenix Token is designed for that.

I put two methods in App.Guardian for convenient like this:

# lib/app/guardian.ex@salt "user salt"# encrypted_token as Phoenix Token
def sign_token(token) do
Phoenix.Token.sign(AppWeb.Endpoint, @salt, token)
end
def verify_token(signed_token) do
Phoenix.Token.verify(AppWeb.Endpoint, @salt, signed_token, max_age: 86400)
end

I use a plug to generate the token from Guardian JWT token like this:

# lib/app_web/plugs/sign_token.exdefmodule AppWeb.Plug.SignToken do
import Plug.Conn
import Guardian.Plug
def init(_params) do
end
def call(conn, _params) do
token = Guardian.Plug.current_token(conn)
signed_token = App.Guardian.sign_token(token)
assign(conn, :signed_token, signed_token)
end
end

Please note that Phoenix.Token is “signed to prevent tampering but not encrypted”. You might consider to use a more dynamic token then the one provided by Guardian.Plug.

And my routes look like this

pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :auth do
plug Guardian.Plug.Pipeline, module: App.Guardian,
error_handler: AppWeb.SessionController
plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
# if there isn't anyone logged in we don't want to return an error. Use allow_blank
plug Guardian.Plug.LoadResource, allow_blank: true
plug AppWeb.Plug.SignToken
end
pipeline :ensure_auth do
plug Guardian.Plug.EnsureAuthenticated
plug AppWeb.Plug.CurrentUser
plug AppWeb.Plug.SignToken
end

Then this token can be put into Phoenix template like this

# lib/app_web/templates/layout/app.html<head>
<meta name='token' content="<%= @conn.assigns[:signed_token] %>" />
</head>

If there is a vue application, token can be retrieved like this

# assets/src/App.Vuemounted: function() {
let token = document.head.querySelector("meta[name='token']").getAttribute("content")
let socket = new Socket("/socket", {params: {token: token}})
socket.onError(error => {
console.log(error)
})
socket.connect() this.channel = socket.channel("chat:lobby", {})
this.channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
},

And user socket in Phoenix can verify the token like this

# lib/app_web/channels/user_socket.exdef connect(%{"token" => signed_token} = params, socket, _connect_info) do
case App.Guardian.verify_token(signed_token) do
{:ok, jwt} ->
case Guardian.Phoenix.Socket.authenticate(socket, App.Guardian, jwt) do
{:ok, authed_socket} ->
Logger.info("Authenticated ? #{Guardian.Phoenix.Socket.authenticated?(authed_socket)}")
{:ok, authed_socket}
{:error, _} ->
Logger.info("Authenticated ? #{Guardian.Phoenix.Socket.authenticated?(socket)}")
{:ok, socket}
# :error # To rejected unauthenticated socket, uncomment this one
end
{:error, _} -> {:ok, socket}
end
end

Guardian.Phoenix.Socket works like Guardian.Plug. And it can also check whether the socket is authenticated, which is often convenient for us. For example, you can get signed-in user in channel like this:

defmodule AppWeb.ChatChannel do
use Phoenix.Channel
require Logger def join("chat:lobby", auth_msg, socket) do
case Guardian.Phoenix.Socket.current_resource(socket) do
%App.Accounts.User{email: email} ->
Logger.info(#{inspect email} is signed in)
_ -> nil
end
{:ok, socket}
end
end

At this point, everything can be authenticated from phoenix views or channel.

But in my case, the authentication is not verified by our own phoenix application, but through XMPP server. Therefore, we need to have GenServer for that. A very simple GenServer as XMPP client will do:

# lib/app/xmpp/client.exalias Romeo.Stanza
alias Romeo.Connection
alias Romeo.Roster
defmodule App.XMPP.Client do
use GenServer
require Logger defmodule State do
defstruct conn: nil,
opts: nil,
connected_user: nil
end
def start_link(%{username: username, password: password} = args) do
Logger.info("Start Client #{inspect args}")
name = App.XMPP.Utilities.via_tuple(username)
GenServer.start_link(__MODULE__, args, name: name)
end
# Callback def init(%{username: username, password: password} = opts) do
opts = [jid: username, password: password]
{:ok, conn} = Romeo.Connection.start_link(opts)
{:ok, %State{conn: conn, opts: opts, connected_user: nil}}
end
def handle_info(:connection_ready, %{conn: conn, opts: opts} = state) do
Logger.info("connection ready #{inspect opts}")
jid = Romeo.JID.bare(opts[:jid]) {:noreply, Map.put(state, :connected_user, jid)}
end

Few utilities methods for name registration of GenServer

# lib/app/xmpp.utilities.exdefmodule App.XMPP.Utilities do  @xmpp_client_registry :xmpp_client_registry  def xmpp_client_registry, do: @xmpp_client_registry  # return first xmpp client by username
def xmpp_client_by(username) do
clients = Registry.lookup(App.XMPP.Utilities.xmpp_client_registry, username)
{client, _} = Enum.at(clients, 0)
client
end
def via_tuple(client_id) do
{:via, Registry, {xmpp_client_registry, client_id}}
end
end

Once the messages “connection _ready” is received, it means the XMPP server verifies the user.

In order to use this GenServer, we need a dynamic supervisor like this:

defmodule App.XMPP.Supervisor do
use DynamicSupervisor
def start_link(args) do
DynamicSupervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init(_args) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def start_child(%{username: username, password: password} = params) do
child_spec = {App.XMPP.Client, %{username: username, password: password}}
DynamicSupervisor.start_child(__MODULE__, child_spec)
end
end

And add this supervisor in Phoenix application to start the supervisor at beginning:

# lib/app/application.exdef start(_type, _args) do
# List all child processes to be supervised
children = [
...
# Start Registry
{Registry, [keys: :unique, name: :xmpp_client_registry]},
# XMPP supervisor to manage all XMPP managers
App.XMPP.Supervisor
]
end

In any Phoenix controller, you can start the XMPP client (GenServer) like this

App.XMPP.Supervisor.start_child(%{username: username, password: password]})

Please note that if you use regular supervisor or genserver, once the controller method ends, the supervisor or genserver will end with it. Therefore, you need a dynamic supervisor in Phoenix application and start the genserver from dynamic supervisor to avoid this problem.

To know whether the XMPP server returned “connection_ready”, you can first find the corresponding GenServer for a given user, and check the status through messaging like this:

# in Phoenix controllerclient = App.XMPP.Utilities.xmpp_client_by(username)
case GenServer.call(client, "connected?") do
...
end

The corresponding callback in GenServer looks like this:

# lib/app/xmpp/client.exdef handle_call("connected?", _from, %{connected_user: nil} = state) do
Logger.info("not yet connected")
{:reply, {:not_connected}, state}
end
def handle_call("connected?", _from, %{connected_user: user} = state) do
Logger.info("connected #{inspect user}")
{:reply, {:connected, user}, state}
end

Please note that when message “connection_ready” is received, we add a fiend :connected_user into state. Therefore, we can retrieve this information in the callback.

You can also use asynchronous call (GenServer.cast) and broadcast the result to Vue through channel directly without going through Phoenix controller. GenServer is persistent with Phoenix application and Vue is persistent with user browser, but controller method is not. Communication between GenServer and Vue directly might be better.

--

--

Responses (1)