Packages
goth
1.3.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.3.0-rc.5
1.3.0-rc.4
1.3.0-rc.3
1.3.0-rc.2
1.3.0-rc.1
1.3.0-rc.0
1.2.0
1.1.0
1.0.1
1.0.1-beta
1.0.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.3
0.0.2
0.0.1
A simple library to generate and retrieve Oauth2 tokens for use with Google Cloud Service accounts.
Current section
Files
Jump to
Current section
Files
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