Skip to content

Commit

Permalink
Support fetching subject token from url for workload identity (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
slackersoft authored Dec 20, 2024
1 parent 8c3431f commit 0226efa
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 11 deletions.
47 changes: 36 additions & 11 deletions lib/goth/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ defmodule Goth.Token do
* `{:refresh_token, credentials}` - for fetching token using refresh token
* `{:workload_identity, credentials}` - for fetching token using workload identity
* `:metadata` - for fetching token using Google internal metadata service
If `:source` is not set, Goth will:
Expand Down Expand Up @@ -118,6 +120,25 @@ defmodule Goth.Token do
* `:url` - the URL of the authentication service, defaults to:
`"https://www.googleapis.com/oauth2/v4/token"`
#### Workload identity - `{:workload_identity, credentials}`
The `credentials` is a map and can contain the following keys:
* `"token_url"`
* `"audience"`
* `"subject_token_type"`
* `"credential_source"` - information about how to retrieve the subject token, can contain the following keys:
* `"format"` - including a `"type"` of `"json"` or `"text"` and optionally `"subject_token_field_name"` if `"json"`
* `"file"` - file to read the subject token from
* `"url"` - url to fetch the subject token from
* `"headers"` - any headers to pass to the url
#### Google metadata server - `:metadata`
Same as `{:metadata, []}`
Expand Down Expand Up @@ -367,7 +388,7 @@ defmodule Goth.Token do
"requested_token_type" => "urn:ietf:params:oauth:token-type:access_token",
"scope" => "https://www.googleapis.com/auth/cloud-platform",
"subject_token_type" => subject_token_type,
"subject_token" => subject_token_from_credential_source(credential_source)
"subject_token" => subject_token_from_credential_source(credential_source, config)
})

response = request(config.http_client, method: :post, url: token_url, headers: headers, body: body)
Expand All @@ -390,23 +411,27 @@ defmodule Goth.Token do
{url, audience}
end

defp subject_token_from_credential_source(%{"file" => file, "format" => format}) do
binary = File.read!(file)

case format do
%{"type" => "text"} ->
binary

%{"type" => "json", "subject_token_field_name" => field} ->
binary |> Jason.decode!() |> Map.fetch!(field)
defp subject_token_from_credential_source(%{"url" => url, "headers" => headers, "format" => format}, config) do
with {:ok, %{status: 200, body: body}} <-
request(config.http_client, method: :get, url: url, headers: Enum.to_list(headers), body: "") do
subject_token_from_binary(body, format)
end
end

defp subject_token_from_credential_source(%{"file" => file, "format" => format}, _config) do
File.read!(file) |> subject_token_from_binary(format)
end

# the default file type if not specified is "text"
defp subject_token_from_credential_source(%{"file" => file}) do
defp subject_token_from_credential_source(%{"file" => file}, _config) do
File.read!(file)
end

defp subject_token_from_binary(binary, %{"type" => "text"}), do: binary

defp subject_token_from_binary(binary, %{"type" => "json", "subject_token_field_name" => field}),
do: binary |> Jason.decode!() |> Map.fetch!(field)

defp handle_jwt_response({:ok, %{status: 200, body: body}}) do
{:ok, build_token(%{"id_token" => body})}
end
Expand Down
17 changes: 17 additions & 0 deletions test/data/test-credentials-url-workload-identity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"audience": "//iam.googleapis.com/projects/my-project/locations/global/workloadIdentityPools/my-cluster/providers/my-provider",
"credential_source": {
"url": "",
"headers": {
"Authentication": "Bearer 123",
"X-Other": "things"
},
"format": {
"type": "json",
"subject_token_field_name": "token"
}
},
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"type": "external_account"
}
35 changes: 35 additions & 0 deletions test/goth/token_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,41 @@ defmodule Goth.TokenTest do
assert token.scope == nil
end

test "fetch/1 from url-based workload identity" do
token_bypass = Bypass.open()
subject_token_bypass = Bypass.open()

Bypass.expect(subject_token_bypass, fn conn ->
assert conn.request_path == "/get/credentials"

body = File.read!("test/data/workload-identity-token.json")
Plug.Conn.resp(conn, 200, body)
end)

Bypass.expect(token_bypass, fn conn ->
assert conn.request_path == "/v1/token"

body = ~s|{"access_token":"dummy","expires_in":599,"token_type":"Bearer"}|
Plug.Conn.resp(conn, 200, body)
end)

credentials =
File.read!("test/data/test-credentials-url-workload-identity.json")
|> Jason.decode!()
|> Map.put("token_url", "http://localhost:#{token_bypass.port}/v1/token")
|> Map.update!("credential_source", fn source ->
Map.put(source, "url", "http://localhost:#{subject_token_bypass.port}/get/credentials")
end)

config = %{
source: {:workload_identity, credentials}
}

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

defp random_service_account_credentials do
%{
"private_key" => random_private_key(),
Expand Down

0 comments on commit 0226efa

Please sign in to comment.