defmodule Ueberauth.Strategy.Duo do
use Ueberauth.Strategy,
uid_field: :sub,
default_scope: "openid email profile"
alias Plug.Conn
alias Ueberauth.Auth.{Info, Credentials, Extra}
alias Ueberauth.Strategy.Duo.OAuth
@impl Ueberauth.Strategy
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)
params = [scope: scopes] |> with_state_param(conn)
opts = options(conn) |> Keyword.put(:redirect_uri, callback_url(conn))
redirect!(conn, OAuth.authorize_url!(params, opts))
end
@impl Ueberauth.Strategy
def handle_callback!(%Conn{params: %{"code" => code}} = conn) do
opts = options(conn) |> Keyword.put(:redirect_uri, callback_url(conn))
client = OAuth.get_token!([code: code], opts)
token = client.token
case token do
nil ->
err = token.other_params["error"]
desc = token.other_params["error_description"]
set_errors!(conn, [error(err, desc)])
_token ->
fetch_user(conn, token)
end
rescue
err in [Error] ->
set_errors!(conn, [error("OAuth2", err.reason)])
end
@impl Ueberauth.Strategy
def handle_callback!(%Conn{params: %{"error" => key, "error_description" => message}} = conn) do
set_errors!(conn, [error(key, message)])
end
@doc """
Cleans up the private area of the connection used for passing the raw Duo response around during the callback.
"""
@impl Ueberauth.Strategy
def handle_cleanup!(conn) do
conn
|> put_private(:duo_user, nil)
|> put_private(:duo_token, nil)
end
@doc """
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
"""
@impl Ueberauth.Strategy
def info(conn) do
user = conn.private.duo_user
%Info{
email: user["email"],
first_name: user["given_name"],
last_name: user["family_name"],
name: user["name"],
}
end
@doc """
Stores the raw information (including the token) obtained from the Duo callback.
"""
@impl Ueberauth.Strategy
def extra(conn) do
%Extra{
raw_info: %{
token: conn.private.duo_token,
user: conn.private.duo_user
}
}
end
@doc """
Includes the credentials from the Duo response.
"""
@impl Ueberauth.Strategy
def credentials(conn) do
token = conn.private.duo_token
%Credentials{
token: token.access_token,
token_type: token.token_type,
refresh_token: token.refresh_token,
expires: token.expires_at != nil,
expires_at: token.expires_at,
scopes: token.other_params["scope"]
}
end
@doc """
Fetches the uid field from the Duo response. This defaults to the option `uid_field` which in-turn defaults to `sub`
"""
@impl Ueberauth.Strategy
def uid(conn) do
conn
|> option(:uid_field)
|> to_string()
|> fetch_uid(conn)
end
defp fetch_uid(field, conn) do
conn.private.duo_user[field]
end
defp fetch_user(conn, token) do
conn = put_private(conn, :duo_token, token)
opts = options(conn) |> Keyword.put(:token, token)
with {:ok, user} <- Ueberauth.Strategy.Duo.OAuth.get_user_info(_headers = [], opts) do
put_private(conn, :duo_user, user)
else
{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", inspect(reason))])
{:error, %{status_code: 401}} ->
set_errors!(conn, [error("Duo token [401]", "unauthorized")])
{:error, %{status_code: status, body: body}} when status in 400..599 ->
set_errors!(conn, [error("Duo [#{status}]", inspect(body))])
end
end
defp option(conn, key) do
default = Keyword.get(default_options(), key)
conn
|> options()
|> Keyword.get(key, default)
end
end