Hex.pmHexdocs.pmGithub.com

Überauth DocuSign

DocuSign OAuth2 strategy for Überauth.

Complete OAuth2 integration for DocuSign authentication in your Elixir applications. This library enables secure user authentication and authorization through DocuSign's OAuth2 implementation, perfect for applications that need to integrate with DocuSign's eSignature APIs.

Features

Installation

Add ueberauth_docusign to your list of dependencies in mix.exs:

def deps do
[
{:ueberauth, "~> 0.10"},
{:ueberauth_docusign, "~> 1.0"},
# Optional: For making API calls after authentication
{:docusign, "~> 3.0"}
]
end

Configuration

1. Configure Überauth

Add DocuSign to your Überauth configuration:

# config/config.exs
config :ueberauth, Ueberauth,
providers: [
docusign: {Ueberauth.Strategy.DocuSign, [
default_scope: "signature extended",
environment: "demo" # or "production"
]}
]

2. Configure OAuth Credentials

Add your DocuSign OAuth credentials:

# config/runtime.exs or config/dev.exs
config :ueberauth, Ueberauth.Strategy.DocuSign.OAuth,
client_id: System.get_env("DOCUSIGN_CLIENT_ID"),
client_secret: System.get_env("DOCUSIGN_CLIENT_SECRET")

To get your credentials:

  1. Go to DocuSign Developer Center
  2. Create a new integration or use an existing one
  3. Copy your Integration Key (client_id) and Secret Key (client_secret)
  4. Add your redirect URI: https://yourapp.com/auth/docusign/callback

3. Set Up Routes

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug Ueberauth
end
scope "/auth", MyAppWeb do
pipe_through :browser
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
post "/:provider/callback", AuthController, :callback
end
end

4. Implement Controller

# lib/my_app_web/controllers/auth_controller.ex
defmodule MyAppWeb.AuthController do
use MyAppWeb, :controller
def callback(%{assigns: %{ueberauth_failure: fails}} = conn, _params) do
conn
|> put_flash(:error, "Failed to authenticate: #{inspect(fails)}")
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
case create_or_update_user(auth) do
{:ok, user} ->
conn
|> put_flash(:info, "Successfully authenticated!")
|> put_session(:current_user, user)
|> configure_session(renew: true)
|> redirect(to: "/dashboard")
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/")
end
end
defp create_or_update_user(%Ueberauth.Auth{} = auth) do
# Extract user information
%{
uid: auth.uid,
email: auth.info.email,
name: auth.info.name,
token: auth.credentials.token,
refresh_token: auth.credentials.refresh_token,
expires_at: auth.credentials.expires_at,
accounts: auth.extra.raw_info.accounts
}
|> find_or_create_user()
end
end

Advanced Configuration

OAuth Scopes

DocuSign supports various OAuth scopes to control access:

config :ueberauth, Ueberauth,
providers: [
docusign: {Ueberauth.Strategy.DocuSign, [
default_scope: "signature extended impersonation"
]}
]

Available scopes:

Environment Configuration

Switch between demo (sandbox) and production environments:

# Demo/Sandbox environment (default)
config :ueberauth, Ueberauth,
providers: [
docusign: {Ueberauth.Strategy.DocuSign, [environment: "demo"]}
]
# Production environment
config :ueberauth, Ueberauth,
providers: [
docusign: {Ueberauth.Strategy.DocuSign, [environment: "production"]}
]

Request Options

You can pass additional parameters in the authorization request:

# In your controller or view
def login(conn, _params) do
conn
|> redirect(to: Routes.auth_path(conn, :request, :docusign,
scope: "signature extended",
login_hint: "user@example.com",
prompt: "login" # Forces re-authentication
))
end

Integration with DocuSign API Client

After successful authentication, use the access token with the docusign client:

def handle_docusign_auth(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
# Get the default account
account = Enum.find(auth.extra.raw_info.accounts, & &1["is_default"])
# Create a connection for API calls
{:ok, api_conn} = DocuSign.Connection.from_oauth_client(
build_oauth_client(auth.credentials.token),
account_id: account["account_id"],
base_uri: account["base_uri"] <> "/restapi"
)
# Now you can make API calls
{:ok, envelopes} = DocuSign.Api.Envelopes.envelopes_get_envelopes(
api_conn,
account["account_id"]
)
end
defp build_oauth_client(access_token) do
OAuth2.Client.new(
token: %OAuth2.AccessToken{
access_token: access_token,
token_type: "Bearer"
}
)
end

Working with Multiple Accounts

DocuSign users may have access to multiple accounts. Handle account selection:

def handle_multiple_accounts(auth) do
accounts = auth.extra.raw_info.accounts
# Get the default account
default_account = Enum.find(accounts, & &1["is_default"])
# Or let user choose
Enum.map(accounts, fn account ->
%{
id: account["account_id"],
name: account["account_name"],
base_uri: account["base_uri"],
is_default: account["is_default"]
}
end)
end

Token Refresh

Handle token expiration and refresh:

def refresh_token(refresh_token) do
client = OAuth2.Client.new(
strategy: OAuth2.Strategy.Refresh,
client_id: Application.fetch_env!(:ueberauth, Ueberauth.Strategy.DocuSign.OAuth)[:client_id],
client_secret: Application.fetch_env!(:ueberauth, Ueberauth.Strategy.DocuSign.OAuth)[:client_secret],
site: "https://account.docusign.com",
token_url: "/oauth/token",
params: %{"refresh_token" => refresh_token}
)
case OAuth2.Client.get_token(client) do
{:ok, %OAuth2.Client{token: new_token}} ->
{:ok, new_token}
{:error, error} ->
{:error, error}
end
end

Running Integration Tests

Integration tests are excluded from the default test run. To run them, set DOCUSIGN_CLIENT_ID and DOCUSIGN_CLIENT_SECRET in the environment and run:

mix test --include integration

Tests that require credentials skip cleanly when either variable is absent.

Security Considerations

CSRF Protection

The library includes built-in CSRF protection via the state parameter. Always verify the state parameter in production:

# Generate a secure state parameter
state = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
# Store in session before redirect
conn
|> put_session(:oauth_state, state)
|> redirect(to: Routes.auth_path(conn, :request, :docusign, state: state))
# Verify in callback
def callback(conn, %{"state" => state} = params) do
if state == get_session(conn, :oauth_state) do
# Process callback
else
# Reject - possible CSRF attack
end
end

Secure Storage

Error Handling

The library provides detailed error information in the ueberauth_failure struct:

def callback(%{assigns: %{ueberauth_failure: fails}} = conn, _params) do
case fails.errors |> List.first() do
%{message: "access_denied"} ->
# User denied authorization
%{message: "invalid_grant"} ->
# Invalid or expired authorization code
%{message_key: "missing_code"} ->
# No authorization code received
_ ->
# Other errors
end
end

Testing

The library includes comprehensive test coverage. Run tests with:

mix test

For integration tests with real DocuSign API:

# Copy and configure test credentials
cp config/test.secret.exs.template config/test.secret.exs
# Run integration tests
mix test --only integration

Development

Setting Up Development Environment

# Clone the repository
git clone https://github.com/neilberkman/ueberauth_docusign.git
cd ueberauth_docusign
# Install dependencies
mix deps.get
# Run tests
mix test
# Run quality checks
mix format --check-formatted
mix credo --strict
mix dialyzer

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin feature/my-new-feature)
  5. Create a new Pull Request

Troubleshooting

Common Issues

Invalid Grant Error

Consent Required

Token Expiration

License

MIT License. See LICENSE for details.

Acknowledgments

This library is part of the DocuSign Elixir ecosystem, designed to work seamlessly with the docusign API client library.