Current section

Files

Jump to
goth lib goth client.ex
Raw

lib/goth/client.ex

defmodule Goth.Client do
@moduledoc false
# `Goth.Client` is the module through which all interaction with Google's APIs flows.
# For the most part, you probably don't want to use this module directly, but instead
# use the other modules that cache and wrap the underlying API calls.
#
# ## Available Options
#
# The first parameter is either the token scopes or a tuple of the service
# account client email and its scopes.
#
# Additional token attributes are controlled through options. Available values:
#
# - `iat` - The time the assertion was issued, default to now.
# - `sub` - The email address of the user for which the application is requesting delegated access.
# Default values is taken from the config `:actor_email`.
#
# See
# [Google's Documentation](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests)
# for more details.
alias Goth.Config
alias Goth.Token
# *Note:* Most often, you'll want to use `Goth.Token.for_scope/1` instead of this method.
# As the docs for `Goth.Token.for_scope/1` note, it will return a cached token if one
# already exists, thus saving you the cost of a round-trip to the server to generate a
# new token.
#
# `Goth.Client.get_access_token/1`, on the other hand will always hit the server to
# retrieve a new token.
def get_access_token(scope), do: get_access_token({:default, scope}, [])
def get_access_token(scope, opts) when is_binary(scope) and is_list(opts) do
get_access_token({:default, scope}, opts)
end
def get_access_token({account, scope}, opts) when is_binary(scope) and is_list(opts) do
{:ok, token_source} = Config.get(account, :token_source)
get_access_token(token_source, {account, scope}, opts)
end
@doc false
def get_access_token(source, info, opts \\ [])
# Fetch an access token from Google's metadata service for applications running
# on Google's Cloud platform.
def get_access_token(type, scope, opts) when is_atom(type) and is_binary(scope) do
get_access_token(type, {:default, scope}, opts)
end
def get_access_token(:metadata, {service_account, scope}, _opts) do
headers = [{"Metadata-Flavor", "Google"}]
account = Application.get_env(:goth, :metadata_account, "default")
metadata = Application.get_env(:goth, :metadata_url, "http://metadata.google.internal")
endpoint = "computeMetadata/v1/instance/service-accounts"
url_base = "#{metadata}/#{endpoint}/#{account}"
url = "#{url_base}/token"
response = request!(:get, url, headers, "")
{:ok, Token.from_response_json({service_account, scope}, response.body)}
end
# Fetch an access token from Google's OAuth service using a JWT
def get_access_token(:oauth_jwt, {account, scope}, opts) do
%{sub: sub} = destruct_opts(opts)
endpoint = Application.get_env(:goth, :endpoint, "https://www.googleapis.com")
url = "#{endpoint}/oauth2/v4/token"
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
body = "grant_type=#{grant_type}&assertion=#{jwt({account, scope}, opts)}"
headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
request(:post, url, headers, body)
|> handle_response({account, scope}, sub)
end
# Fetch an access token from Google's OAuth service using a refresh token
def get_access_token(:oauth_refresh, {account, scope}, _opts) do
{:ok, refresh_token} = Config.get(:refresh_token)
{:ok, client_id} = Config.get(:client_id)
{:ok, client_secret} = Config.get(:client_secret)
endpoint = Application.get_env(:goth, :endpoint, "https://www.googleapis.com")
url = "#{endpoint}/oauth2/v4/token"
body =
URI.encode_query(
grant_type: "refresh_token",
refresh_token: refresh_token,
client_id: client_id,
client_secret: client_secret
)
headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
request(:post, url, headers, body)
|> handle_response({account, scope})
end
def claims(scope, opts \\ [])
def claims(scope, iat) when is_integer(iat), do: claims(scope, iat: iat)
def claims(scope, opts) when is_binary(scope), do: claims({:default, scope}, opts)
def claims({account, scope}, opts) when is_list(opts) do
%{iat: iat, sub: sub} = destruct_opts(opts)
{:ok, email} = Config.get(account, :client_email)
c = %{
"iss" => email,
"scope" => scope,
"aud" => "https://www.googleapis.com/oauth2/v4/token",
"iat" => iat,
"exp" => iat + 10
}
if sub do
Map.put(c, "sub", sub)
else
c
end
end
def json(scope, opts \\ [])
def json(scope, iat) when is_integer(iat), do: json(scope, iat: iat)
def json(scope, opts) when is_binary(scope), do: json({:default, scope}, opts)
def json({account, scope}, opts) when is_list(opts) do
claims({account, scope}, opts)
|> Jason.encode!()
end
def jwt(info, opts \\ [])
def jwt(scope, iat) when is_integer(iat), do: jwt(scope, iat: iat)
def jwt(scope, opts) when is_binary(scope), do: jwt({:default, scope}, opts)
def jwt({account, scope}, opts) when is_list(opts) do
{:ok, key} = Config.get(account, :private_key)
jwk = JOSE.JWK.from_pem(key)
header = %{"alg" => "RS256", "typ" => "JWT"}
claim_set = claims({account, scope}, opts)
jwk
|> JOSE.JWT.sign(header, claim_set)
|> JOSE.JWS.compact()
|> elem(1)
end
@doc "Retrieves the project ID from Google's metadata service"
def retrieve_metadata_project do
headers = [{"Metadata-Flavor", "Google"}]
endpoint = "computeMetadata/v1/project/project-id"
metadata = Application.get_env(:goth, :metadata_url, "http://metadata.google.internal")
url = "#{metadata}/#{endpoint}"
request!(:get, url, headers, "").body
end
defp destruct_opts(opts) do
defaults = [
iat: :os.system_time(:seconds),
sub:
case Config.get(:actor_email) do
{:ok, sub} -> sub
_ -> nil
end
]
defaults
|> Keyword.merge(opts)
|> Enum.into(%{})
end
defp request(method, url, headers, body) do
Goth.__finch__(method: method, url: url, headers: headers, body: body)
end
defp request!(method, url, headers, body) do
case request(method, url, headers, body) do
{:ok, response} -> response
{:error, exception} -> raise exception
end
end
defp handle_response(resp, opts, sub \\ nil)
defp handle_response({:ok, %{body: body, status: code}}, {account, scope}, sub)
when code in 200..299,
do: {:ok, Token.from_response_json({account, scope}, sub, body)}
defp handle_response({:ok, %{body: body}}, _scope, _sub),
do: {:error, "Could not retrieve token, response: #{body}"}
defp handle_response(other, _scope, _sub), do: other
end