defmodule DocuSign.OAuth.Impl do
@moduledoc ~S"""
This module implements the OAuth behaviour and an oauth2 strategy for DocuSign.
### Examples
client = DocuSign.OAuth.Impl.client() |> DocuSign.OAuth.Impl.get_token!()
{:ok, user_info } = OAuth2.Client.get(client, "/oauth/userinfo")
"""
use OAuth2.Strategy
@behaviour DocuSign.OAuth
alias OAuth2.{AccessToken, Client, Error}
@type param :: binary | %{binary => param} | [param]
@type params :: %{binary => param} | Keyword.t()
@type headers :: [{binary, binary}]
@grant_type "urn:ietf:params:oauth:grant-type:jwt-bearer"
@client_info_path "/oauth/userinfo"
@impl DocuSign.OAuth
def client(opts \\ []) do
user_id = Keyword.get(opts, :user_id, get_default_user_id())
client_id = Application.fetch_env!(:docusign, :client_id)
hostname = Application.fetch_env!(:docusign, :hostname)
token_expires_in = Application.get_env(:docusign, :token_expires_in, 2 * 60 * 60)
[
strategy: __MODULE__,
client_id: client_id,
ref: %{
user_id: user_id,
hostname: hostname,
token_expires_in: token_expires_in
},
site: "https://#{hostname}",
authorize_url: "oauth/auth?response_type=code&scope=signature%20impersonation"
]
|> Keyword.merge(opts)
|> Client.new()
|> Client.put_serializer("application/json", Poison)
end
defp get_default_user_id do
Application.get_env(:docusign, :user_id)
end
@impl DocuSign.OAuth
def get_token!(client, params \\ [], headers \\ [], opts \\ []) do
Client.get_token!(client, params, headers, opts)
end
@impl DocuSign.OAuth
def refresh_token!(client, force \\ false) do
if force || token_expired?(client) do
Client.get_token!(client)
else
client
end
end
@impl DocuSign.OAuth
def interval_refresh_token(client),
do: client.token.expires_at - :erlang.system_time(:second) - 10
@impl DocuSign.OAuth
def token_expired?(%AccessToken{} = token), do: AccessToken.expired?(token)
def token_expired?(nil), do: true
def token_expired?(%Client{token: nil}), do: true
def token_expired?(%Client{token: token}), do: token_expired?(token)
@impl DocuSign.OAuth
def get_client_info(client) do
case Client.get(client, @client_info_path) do
{:ok, %{body: body}} -> body
_error -> nil
end
end
@impl OAuth2.Strategy
@spec get_token(OAuth2.Client.t(), any, any) :: OAuth2.Client.t()
def get_token(client, _params, _headers) do
client
|> put_param(:grant_type, @grant_type)
|> put_param(:assertion, assertion(client))
end
# OAuth2.Strategy callback
@impl OAuth2.Strategy
def authorize_url(_client, _params) do
raise Error, reason: "This strategy does not implement `authorize_url`."
end
# Create claim and sign with private key
#
@spec assertion(Client.t()) :: binary | no_return()
defp assertion(client) do
client
|> claims()
|> generate_and_sign!()
end
defp claims(client) do
now_unix = :erlang.system_time(:second)
%{
"iss" => client.client_id,
"sub" => client.ref.user_id,
"aud" => client.ref.hostname,
"iat" => now_unix,
"exp" => now_unix + client.ref.token_expires_in,
"scope" => "signature"
}
end
# Signed payload use token key
#
@spec generate_and_sign!(map) :: binary | no_return()
defp generate_and_sign!(claims) do
Joken.generate_and_sign!(%{}, claims, token_signer())
end
# Take token signer from application env and if it doesn't exist, create it.
#
@spec token_signer :: Joken.Signer.t()
defp token_signer do
case Application.fetch_env(:docusign, :token_signer) do
{:ok, token_signer} ->
token_signer
:error ->
token_signer = create_token_signer()
Application.put_env(:docusign, :token_signer, token_signer)
token_signer
end
end
# Create token signer based on PEM-encoded key. If the provided `pem_key` is
# `nil`, load it from the application environment.
#
@spec create_token_signer :: Joken.Signer.t()
defp create_token_signer do
Joken.Signer.create("RS256", %{"pem" => token_key()})
end
defp token_key do
:docusign
|> Application.fetch_env!(:private_key)
|> File.read!()
end
end