# Copyright (c) Cratis. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
defmodule Chronicle.Connections.Auth do
@moduledoc false
# Fetches OAuth2 Bearer tokens using the client credentials grant via HTTP/2.
require Logger
@doc """
Fetches an OAuth2 access token using the client credentials grant.
POSTs to `http(s)://host:port/connect/token` with
`grant_type=client_credentials`, `client_id`, and `client_secret`.
Returns `{:ok, token}` or `{:error, reason}`.
"""
@spec fetch_token(String.t(), non_neg_integer(), String.t(), String.t(), boolean()) ::
{:ok, String.t()} | {:error, term()}
def fetch_token(host, port, client_id, client_secret, disable_tls) do
scheme = if disable_tls, do: :http, else: :https
body =
URI.encode_query(%{
"grant_type" => "client_credentials",
"client_id" => client_id,
"client_secret" => client_secret
})
headers = [
{"content-type", "application/x-www-form-urlencoded"},
{"content-length", Integer.to_string(byte_size(body))},
{"accept", "application/json"}
]
mint_opts = if disable_tls, do: [], else: [transport_opts: [verify: :verify_none]]
with {:ok, conn} <- Mint.HTTP.connect(scheme, host, port, mint_opts),
{:ok, conn, _ref} <- Mint.HTTP.request(conn, "POST", "/connect/token", headers, body),
{:ok, {status, resp_body}} <- receive_response(conn) do
Mint.HTTP.close(conn)
case status do
200 ->
case Jason.decode(resp_body) do
{:ok, %{"access_token" => token}} -> {:ok, token}
{:ok, resp} -> {:error, {:missing_access_token, resp}}
{:error, reason} -> {:error, {:json_decode, reason}}
end
other ->
{:error, {:http_error, other, resp_body}}
end
end
rescue
e -> {:error, {:exception, e}}
end
defp receive_response(conn, status \\ nil, body \\ "") do
receive do
message ->
case Mint.HTTP.stream(conn, message) do
{:ok, conn, responses} ->
{new_status, new_body, done?} =
Enum.reduce(responses, {status, body, false}, fn
{:status, _ref, s}, {_, b, d} -> {s, b, d}
{:data, _ref, d}, {s, b, _} -> {s, b <> d, false}
{:done, _ref}, {s, b, _} -> {s, b, true}
_other, acc -> acc
end)
if done? do
{:ok, {new_status, new_body}}
else
receive_response(conn, new_status, new_body)
end
{:error, _conn, reason, _} ->
{:error, {:stream_error, reason}}
:unknown ->
receive_response(conn, status, body)
end
after
10_000 ->
{:error, :timeout}
end
end
end