Current section
Files
Jump to
Current section
Files
lib/ueberauth/strategy/docusign.ex
defmodule Ueberauth.Strategy.DocuSign do
@moduledoc """
DocuSign Strategy for Überauth.
## Setup
Include the provider in your configuration for Ueberauth:
config :ueberauth, Ueberauth,
providers: [
docusign: {Ueberauth.Strategy.DocuSign, []}
]
Then configure your OAuth app at DocuSign and add the client credentials:
config :ueberauth, Ueberauth.Strategy.DocuSign.OAuth,
client_id: System.get_env("DOCUSIGN_CLIENT_ID"),
client_secret: System.get_env("DOCUSIGN_CLIENT_SECRET")
## Options
The strategy accepts the following options:
* `oauth2_module` - The OAuth2 module to use. Defaults to `Ueberauth.Strategy.DocuSign.OAuth`
* `default_scope` - The default scope to request. Defaults to "signature"
* `default_user_email` - The email address for login hint
* `environment` - The DocuSign environment ("demo" or "production"). Defaults to "production"
* `state` - A state parameter for CSRF protection. Auto-generated if not provided
* `prompt` - Controls the authorization prompt ("login" forces re-authentication)
"""
use Ueberauth.Strategy,
default_scope: "signature",
environment: "production",
uid_field: :sub,
oauth2_module: Ueberauth.Strategy.DocuSign.OAuth
alias Ueberauth.Auth.{Credentials, Extra, Info}
@doc """
Handles the initial request for DocuSign authentication.
This will redirect the user to DocuSign's authorization page.
"""
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)
environment = conn.params["environment"] || option(conn, :environment)
params =
[scope: scopes, response_type: "code"]
|> with_optional(
:login_hint,
conn.params["login_hint"] || option(conn, :default_user_email)
)
|> with_optional(:prompt, conn.params["prompt"] || option(conn, :prompt))
|> maybe_add_state_param(conn)
module = option(conn, :oauth2_module)
redirect!(
conn,
module.authorize_url!(params, site: site_for_environment(environment))
)
end
@doc """
Handles the callback from DocuSign.
This will either set the user information if authentication was successful,
or set error information if it failed.
"""
def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
environment = conn.params["environment"] || option(conn, :environment)
module = option(conn, :oauth2_module)
test_site = option(conn, :test_site)
site = test_site || site_for_environment(environment)
token =
module.get_access_token([code: code], site: site, test_site: test_site)
if token_expired?(token) do
set_errors!(conn, [
error("token", "Token has expired")
])
else
case token do
{:ok, token} ->
fetch_user(conn, token, environment)
{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
{:error, %OAuth2.Response{body: body, status_code: 401}} ->
set_errors!(conn, [error("token", body["error_description"] || "unauthorized")])
{:error, _} ->
set_errors!(conn, [error("OAuth2", "An error occurred")])
end
end
end
def handle_callback!(%Plug.Conn{params: %{"error" => error}} = conn) do
set_errors!(conn, [error("OAuth2", error)])
end
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end
@doc false
def handle_cleanup!(conn) do
conn
|> put_private(:docusign_user, nil)
|> put_private(:docusign_token, nil)
end
@doc """
Fetches the uid field from the response.
The uid is the user's unique identifier at DocuSign.
"""
def uid(conn) do
uid_field = option(conn, :uid_field)
conn.private.docusign_user[to_string(uid_field)]
end
@doc """
Includes the credentials from the DocuSign response.
"""
def credentials(conn) do
token = conn.private.docusign_token
scopes = String.split(token.other_params["scope"] || "", " ")
%Credentials{
expires: !!token.expires_at,
expires_at: token.expires_at,
refresh_token: token.refresh_token,
scopes: scopes,
token: token.access_token,
token_type: token.token_type
}
end
@doc """
Fetches the user information from the DocuSign response.
"""
def info(conn) do
user = conn.private.docusign_user
%Info{
email: user["email"],
first_name: user["given_name"],
image: nil,
last_name: user["family_name"],
name: user["name"],
nickname: nil
}
end
@doc """
Stores all the extra information from the DocuSign response.
"""
def extra(conn) do
%Extra{
raw_info: %{
accounts: conn.private[:docusign_accounts],
token: conn.private.docusign_token,
user: conn.private.docusign_user
}
}
end
defp fetch_user(conn, token, environment) do
conn = put_private(conn, :docusign_token, token)
# Fetch user info
test_site = option(conn, :test_site)
user_url =
if test_site do
"#{test_site}/oauth/userinfo"
else
user_info_url(environment)
end
case option(conn, :oauth2_module).get(
token,
user_url,
[{"Authorization", "Bearer #{token.access_token}"}],
test_site: test_site
) do
{:ok, %OAuth2.Response{body: user}} ->
# Fetch accounts info
conn = put_private(conn, :docusign_user, user)
case fetch_accounts(conn, token, user, environment) do
{:ok, accounts} ->
put_private(conn, :docusign_accounts, accounts)
{:error, _reason} ->
# Continue without accounts - not critical for auth
conn
end
{:error, %OAuth2.Response{status_code: 401}} ->
set_errors!(conn, [error("token", "unauthorized")])
{:error, %OAuth2.Response{body: body}} ->
set_errors!(conn, [error("OAuth2", body["message"] || "Error fetching user")])
{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
end
end
defp fetch_accounts(conn, token, _user, environment) do
test_site = option(conn, :test_site)
user_url =
if test_site do
"#{test_site}/oauth/userinfo"
else
user_info_url(environment)
end
case option(conn, :oauth2_module).get(
token,
user_url,
[{"Authorization", "Bearer #{token.access_token}"}],
test_site: test_site
) do
{:ok, %OAuth2.Response{body: %{"accounts" => accounts}}} ->
{:ok, accounts}
_ ->
{:error, "Failed to fetch accounts"}
end
end
defp option(conn, key) do
request_options = conn.assigns[:ueberauth_request_options] || []
opts =
case List.keyfind(request_options, __MODULE__, 0) do
{__MODULE__, opts} -> opts
_ -> []
end
Keyword.get(opts, key, Keyword.get(default_options(), key))
end
defp site_for_environment("demo"), do: "https://account-d.docusign.com"
defp site_for_environment(_), do: "https://account.docusign.com"
defp user_info_url("demo"), do: "https://account-d.docusign.com/oauth/userinfo"
defp user_info_url(_), do: "https://account.docusign.com/oauth/userinfo"
defp with_optional(opts, _key, nil), do: opts
defp with_optional(opts, key, value), do: Keyword.put(opts, key, value)
defp maybe_add_state_param(opts, conn) do
state = conn.params["state"] || option(conn, :state)
if state do
Keyword.put(opts, :state, state)
else
opts
end
end
defp token_expired?(nil), do: true
defp token_expired?({:ok, %OAuth2.AccessToken{} = token}) do
if token.expires_at do
token.expires_at < System.system_time(:second)
else
false
end
end
defp token_expired?(_), do: false
end