Gmail and MS365 adapters use host-owned token_callback and user_email credentials. DripDrop sends with access tokens but does not read OAuth client secrets, persist refresh tokens, or make refresh requests.

Callback Contract

The callback can be an arity-1 function, {Module, :function}, an arity-0 function, or {Module, :function, args}. Arity-1 callbacks receive the channel adapter. Return values can be a bare token string, %{access_token: token}, %{"access_token" => token}, %{token: token}, or %{"token" => token}. Expiration can be expires_at, expires_in, or omitted.

{:ok, %{access_token: "ya29...", expires_at: DateTime.utc_now() |> DateTime.add(3600)}}

or an error:

{:error, :revoked}

DripDrop caches successful tokens by adapter and provider until expiry. The callback is not invoked while a cached token is valid; tokens without a valid expiry are cached for five minutes.

Hand-Rolled Req Example

defmodule MyApp.GoogleTokens do
  def token(_adapter) do
    {:ok, %{body: body}} =
      Req.post("https://oauth2.googleapis.com/token",
        form: [
          grant_type: "refresh_token",
          refresh_token: System.fetch_env!("GOOGLE_REFRESH_TOKEN"),
          client_id: System.fetch_env!("GOOGLE_CLIENT_ID"),
          client_secret: System.fetch_env!("GOOGLE_CLIENT_SECRET")
        ]
      )

    {:ok, %{access_token: body["access_token"], expires_at: DateTime.add(DateTime.utc_now(), body["expires_in"])}}
  end
end

Tango Example

Tango is a recommended companion library, not a DripDrop dependency.

defmodule MyApp.TangoTokens do
  def token(adapter) do
    connection_id = adapter.credentials["oauth_connection_id"]

    with {:ok, connection} <- Tango.Connection.get_by_external_id(connection_id),
         {:ok, token} <- Tango.Connection.access_token(connection) do
      {:ok, %{access_token: token.value, expires_at: token.expires_at}}
    end
  end
end

Ueberauth Example

Use Ueberauth for login/consent and a host-owned refresh job for token storage. The callback should only fetch the current access token from host storage:

defmodule MyApp.UeberauthTokens do
  def token(adapter) do
    identity = adapter.credentials["oauth_identity"]

    case MyApp.OAuthTokens.current(identity, :google) do
      nil -> {:error, :revoked}
      token -> {:ok, %{access_token: token.access_token, expires_at: token.expires_at}}
    end
  end
end