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