Skip to content

Commit

Permalink
Support fetching tokens from compute instance metadata (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcrumm committed Feb 25, 2021
1 parent 482c28f commit 66af30d
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 2 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ A simple library to generate and retrieve OAuth2 tokens for use with Google Clou
}
```

## Google Compute Metadata

Every compute instance stores its metadata on a metadata server.
Goth can query this metadata server to fetch authentication credentials
for a service account within the instance.

To query the metadata server for an access token, you must configure
the following:

* `url` must be set to the base url for the metadata server.
* `credentials` must be set to a tuple `{:instance, account}`
with the name of the service account to use.

```elixir
defmodule MyApp.Application do
use Application

def start(_type, _args) do
children = [
{Goth, name: MyApp.Goth, url: "http://metadata.google.internal", credentials: {:instance, "default"}}
]

Supervisor.start_link(children, strategy: :one_for_one)
end
end
```

<!-- MDOC !-->

## Upgrading from Goth < 1.3
Expand Down
3 changes: 2 additions & 1 deletion lib/goth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ defmodule Goth do
* `:name` - the name to register the server under.
* `:credentials` - a map of credentials.
* `:credentials` - a map of credentials or a tuple `{:instance, account}` (See
"Google Compute Metadata" section in the module documentation for more information.)
* `:cooldown` - Time in milliseconds between retrying requests, defaults
to `#{@cooldown}`.
Expand Down
12 changes: 11 additions & 1 deletion lib/goth/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ defmodule Goth.Token do
Config may contain the following keys:
* `:credentials` - a map of credentials.
* `:credentials` - a map of credentials or a tuple `{:instance, account}` (See
"Google Compute Metadata" section in the `Goth` module documentation for more information.)
* `:scope` - Token scope, defaults to `#{inspect(@default_scope)}`.
Expand Down Expand Up @@ -88,6 +89,9 @@ defmodule Goth.Token do
end
end

# Override for instance metadata
defp jwt(_scope, {:instance, _} = instance), do: instance

defp jwt(scope, %{
"private_key" => private_key,
"client_email" => client_email,
Expand All @@ -108,6 +112,12 @@ defmodule Goth.Token do
JOSE.JWT.sign(jwk, header, claim_set) |> JOSE.JWS.compact() |> elem(1)
end

defp request(http_client, url, {:instance, account}) do
headers = [{"metadata-flavor", "Google"}]
url = "#{url}/computeMetadata/v1/instance/#{account}/token"
Goth.HTTPClient.request(http_client, :get, url, headers, "", [])
end

defp request(http_client, url, jwt) do
headers = [{"content-type", "application/x-www-form-urlencoded"}]
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
Expand Down
19 changes: 19 additions & 0 deletions test/goth/token_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ defmodule Goth.TokenTest do
{:error, :econnrefused} = Goth.Token.fetch(config)
end

test "fetch/1 from instance metadata" do
bypass = Bypass.open()

Bypass.expect(bypass, fn conn ->
assert conn.request_path =~ ~r[/computeMetadata/v1/instance/default/token]
body = ~s|{"access_token":"dummy","expires_in":3599,"token_type":"Bearer"}|
Plug.Conn.resp(conn, 200, body)
end)

config = %{
credentials: {:instance, "default"},
url: "http://localhost:#{bypass.port}",
scope: nil
}

{:ok, token} = Goth.Token.fetch(config)
assert token.token == "dummy"
end

defp random_credentials() do
%{
"private_key" => random_private_key(),
Expand Down
22 changes: 22 additions & 0 deletions test/goth_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ defmodule GothTest do
{:ok, ^token} = Goth.fetch(test)
end

test "fetch/1 using metadata credentials", %{test: test} do
now = System.system_time(:second)
bypass = Bypass.open()

Bypass.expect(bypass, fn conn ->
body = ~s|{"access_token":"dummy","expires_in":3599,"token_type":"Bearer"}|
Plug.Conn.resp(conn, 200, body)
end)

start_supervised!(
{Goth, name: test, credentials: {:instance, "default"}, url: "http://localhost:#{bypass.port}"}
)

{:ok, token} = Goth.fetch(test)
assert token.token == "dummy"
assert token.type == "Bearer"
assert_in_delta token.expires, now + 3599, 1

Bypass.down(bypass)
{:ok, ^token} = Goth.fetch(test)
end

@tag :capture_log
test "retries", %{test: test} do
Process.flag(:trap_exit, true)
Expand Down

0 comments on commit 66af30d

Please sign in to comment.