Skip to content
Permalink
Browse files

Honor credentials from gcloud application defaults

  • Loading branch information...
dazuma committed Oct 5, 2017
1 parent be68c4b commit 4a8b46d9b4232620768c1a6621b8b12ae0eefaa0
@@ -5,4 +5,3 @@
erl_crash.dump
*.ez
/config/*credentials*
!/config/test-credentials.json
@@ -1,6 +1,7 @@
use Mix.Config

config :goth,
json: "config/test-credentials.json" |> Path.expand |> File.read!
json: "test/data/test-credentials.json" |> Path.expand |> File.read!
config :goth, config_root_dir: "test/missing"

# config :bypass, enable_debug_log: true
@@ -39,7 +39,7 @@ defmodule Goth.Client do
end

# Fetch an access token from Google's OAuth service using a JWT
def get_access_token(:oauth, scope) do
def get_access_token(:oauth_jwt, scope) do
endpoint = Application.get_env(:goth, :endpoint, "https://www.googleapis.com")
url = "#{endpoint}/oauth2/v4/token"
body = {:form, [grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
@@ -54,6 +54,27 @@ defmodule Goth.Client do
end
end

# Fetch an access token from Google's OAuth service using a refresh token
def get_access_token(:oauth_refresh, scope) 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 = {:form, [grant_type: "refresh_token",
refresh_token: refresh_token,
client_id: client_id,
client_secret: client_secret]}
headers = [{"Content-Type", "application/x-www-form-urlencoded"}]

{:ok, response} = HTTPoison.post(url, body, headers)
if response.status_code >= 200 && response.status_code < 300 do
{:ok, Token.from_response_json(scope, response.body)}
else
{:error, "Could not retrieve token, response: #{response.body}"}
end
end

def claims(scope), do: claims(scope, :os.system_time(:seconds))
def claims(scope, iat) do
{:ok, email} = Config.get(:client_email)
@@ -25,6 +25,7 @@ defmodule Goth.Config do
config = from_json() ||
from_config() ||
from_creds_file() ||
from_gcloud_adc() ||
from_metadata()
project_id = determine_project_id(config)
{:ok, Map.put(config, "project_id", project_id)}
@@ -49,6 +50,30 @@ defmodule Goth.Config do
end
end

# Search the well-known path for application default credentials provided
# by the gcloud sdk. Note there are different paths for unix and windows.
defp from_gcloud_adc() do
config_root_dir = Application.get_env(:goth, :config_root_dir)
path_root = if config_root_dir == nil do
case :os.type() do
{:win32, _} ->
System.get_env("APPDATA") || ""
{:unix, _} ->
home_dir = System.get_env("HOME") || ""
Path.join([home_dir, ".config"])
end
else
config_root_dir
end

path = Path.join([path_root, "gcloud", "application_default_credentials.json"])
if File.regular?(path) do
path |> File.read!() |> decode_json()
else
nil
end
end

defp from_metadata() do
%{"token_source" => :metadata}
end
@@ -66,7 +91,14 @@ defmodule Goth.Config do
defp decode_json(json) do
json
|> Poison.decode!
|> Map.put("token_source", :oauth)
|> set_token_source
end

defp set_token_source(map = %{"private_key" => _}) do
Map.put(map, "token_source", :oauth_jwt)
end
defp set_token_source(map = %{"refresh_token" => _, "client_id" => _, "client_secret" => _}) do
Map.put(map, "token_source", :oauth_refresh)
end

def set(key, value) when is_atom(key), do: key |> to_string |> set(value)
@@ -0,0 +1,6 @@
{
"type": "authorized_user",
"client_id": "blahblahblah.apps.googleusercontent.com",
"client_secret": "secretsecretsecret",
"refresh_token": "refreshrefreshrefresh"
}
@@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "my-project",
"private_key_id": "12345abcde",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN1IUO2QhgdI+Q\nUTIWV9X7Gm9Cw6kPgHeZ+f+RXXknSL6/CAwVz1EG2VIgCBJv45mF0Vsw2vM8/0Sx\nSDoEb7skelCahYCQUOIp72egAk3InOINGo/n+A1ai7fmR0EzQ6WFr3pZmzcX/7ZB\n0TjDkXX81NJIreJSfqSCvMg7uZfzihv7RbmljyofxoXwP8FoMOS5BcHo8ZkBZyJx\ny0uLKrFvCkTRS/OhuGRVcJC6VrZUA2MhQPkqjHNcttEXIajL+jl4jmVQ8irwR4LO\nXnFaBoEXexciTAteO4vjrrV2iIh0x24vgD2SemhUW/pOTZ/AMNUjwjnvmWFdvvyf\nFYDTtHL1AgMBAAECggEBAILyYBchUpabh6EbFj+CwVGhSnA97e0eE07afpdb0evv\nQg1mBKJuUsUcCLMCQOOFI81lSeiFfmYm2OlFYiuObR50v86qy9RymR1WqDoXZnF+\nR0cJ6yuk3c9niFbYGt6V6lDPfwsUP32s3j1OSjZmKqVQaQYpZPf9bS431jcuV5jF\n0tJEFZTY+FS3BW3JefpDCBW1SmyXtA4BiZdP37I9hKOohC7iQuOna5g9iaCbFKC4\nw80FZngDB4MTpSypjYBOR4SROOcIMd3cXyDJEuYoJqKpc3Ke9QZrHPSZPKREug7u\nG7v5TwFXwn2lLtlV7KXAknl2CUNGHEzDOyMRP0PVZtECgYEA9ywGoPFP2ejMVJ+e\nvsSo5x5mXL0hczYyUT1ryohi1+4Rq4S4StfvLKZ9hKp8xzOOwChV0QNT6ZdLTOQo\nSQWQ0tqZfzIVOBFxeqRPokaojQ0MEvXcDTnUzCSh7q+GvNFkN5kMcOCaPYGsCWzU\np/BYjyijX/SB3Y/vWCIlRWQpQx8CgYEAkuVRtn5nOwlWcykRE/PYZo9HNPcjdW2Q\nAIki1ntfHZTikLRf3cRpWYgWqbYJMiTq4Mkwhye0jgKRVs8urHJwVxYTXyfPl6UM\n17DaCwnX2VuMDEM9cbBxF4MdbIBuQ7YJUmajrh63E/hx2NJso6/nvYk5V4v5v5lZ\nwTKB+7+a+2sCgYEAoWeSfI6YAkhPBgOl+hUZ5rKnTXAD4+REP2DIft1JDpBb4ZEt\nd1JC0Pl3haZ/DOXSFhFA2Ng/d45gkbl7xRNpWwd8rN7blF1vqRKbHfDeKB2ZANij\n9c8J8rUJOYBNkAd8VgIPabaBgiCnYxA6XeBJNFLpPMPB+hj/xqGljQa3GykCgYAo\nLyNTUPDcbYmAp1NMqgAgzkEkdBb3IKmr+9fT5Jv4c6om+7Dd8cUAAQJyGqIZXZAD\nPgZQcsQptPodTT/vXL7uk9NozHM1gKkqt+5t5pttkmWVVS+R0jqdu/honhmL3Fhg\nekN8dlqO1AAQ2D9v58b1SnytPlVr3H95Il/8hkXXUQKBgDvylw5n2rW4ER2j91Lg\nW74L2D9VXIFp8Trrb+QE5G87GQDXq+WaixEScC0tdOV1MnOHQFRLbMzXQcuf34uu\nLu1yTECyOrRwI2tDcCCnNXQx+e10lGhf8sbWTR9jNjWX5QIBiGdOIq7CV8174IuH\nI7pFKB+yxZJd4tT/F4IbrUBU\n-----END PRIVATE KEY-----\n",
"client_email": "test-credentials-2@my-project.iam.gserviceaccount.com",
"client_id": "12345",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/testing-private-key-encryption%40my-project.iam.gserviceaccount.com"
}
@@ -1,12 +1,12 @@
{
"type": "service_account",
"project_id": "tokyo-amphora-437",
"private_key_id": "854414a51270519ed74ec9112389e495eec1ccd1",
"project_id": "my-project",
"private_key_id": "12345abcde",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN1IUO2QhgdI+Q\nUTIWV9X7Gm9Cw6kPgHeZ+f+RXXknSL6/CAwVz1EG2VIgCBJv45mF0Vsw2vM8/0Sx\nSDoEb7skelCahYCQUOIp72egAk3InOINGo/n+A1ai7fmR0EzQ6WFr3pZmzcX/7ZB\n0TjDkXX81NJIreJSfqSCvMg7uZfzihv7RbmljyofxoXwP8FoMOS5BcHo8ZkBZyJx\ny0uLKrFvCkTRS/OhuGRVcJC6VrZUA2MhQPkqjHNcttEXIajL+jl4jmVQ8irwR4LO\nXnFaBoEXexciTAteO4vjrrV2iIh0x24vgD2SemhUW/pOTZ/AMNUjwjnvmWFdvvyf\nFYDTtHL1AgMBAAECggEBAILyYBchUpabh6EbFj+CwVGhSnA97e0eE07afpdb0evv\nQg1mBKJuUsUcCLMCQOOFI81lSeiFfmYm2OlFYiuObR50v86qy9RymR1WqDoXZnF+\nR0cJ6yuk3c9niFbYGt6V6lDPfwsUP32s3j1OSjZmKqVQaQYpZPf9bS431jcuV5jF\n0tJEFZTY+FS3BW3JefpDCBW1SmyXtA4BiZdP37I9hKOohC7iQuOna5g9iaCbFKC4\nw80FZngDB4MTpSypjYBOR4SROOcIMd3cXyDJEuYoJqKpc3Ke9QZrHPSZPKREug7u\nG7v5TwFXwn2lLtlV7KXAknl2CUNGHEzDOyMRP0PVZtECgYEA9ywGoPFP2ejMVJ+e\nvsSo5x5mXL0hczYyUT1ryohi1+4Rq4S4StfvLKZ9hKp8xzOOwChV0QNT6ZdLTOQo\nSQWQ0tqZfzIVOBFxeqRPokaojQ0MEvXcDTnUzCSh7q+GvNFkN5kMcOCaPYGsCWzU\np/BYjyijX/SB3Y/vWCIlRWQpQx8CgYEAkuVRtn5nOwlWcykRE/PYZo9HNPcjdW2Q\nAIki1ntfHZTikLRf3cRpWYgWqbYJMiTq4Mkwhye0jgKRVs8urHJwVxYTXyfPl6UM\n17DaCwnX2VuMDEM9cbBxF4MdbIBuQ7YJUmajrh63E/hx2NJso6/nvYk5V4v5v5lZ\nwTKB+7+a+2sCgYEAoWeSfI6YAkhPBgOl+hUZ5rKnTXAD4+REP2DIft1JDpBb4ZEt\nd1JC0Pl3haZ/DOXSFhFA2Ng/d45gkbl7xRNpWwd8rN7blF1vqRKbHfDeKB2ZANij\n9c8J8rUJOYBNkAd8VgIPabaBgiCnYxA6XeBJNFLpPMPB+hj/xqGljQa3GykCgYAo\nLyNTUPDcbYmAp1NMqgAgzkEkdBb3IKmr+9fT5Jv4c6om+7Dd8cUAAQJyGqIZXZAD\nPgZQcsQptPodTT/vXL7uk9NozHM1gKkqt+5t5pttkmWVVS+R0jqdu/honhmL3Fhg\nekN8dlqO1AAQ2D9v58b1SnytPlVr3H95Il/8hkXXUQKBgDvylw5n2rW4ER2j91Lg\nW74L2D9VXIFp8Trrb+QE5G87GQDXq+WaixEScC0tdOV1MnOHQFRLbMzXQcuf34uu\nLu1yTECyOrRwI2tDcCCnNXQx+e10lGhf8sbWTR9jNjWX5QIBiGdOIq7CV8174IuH\nI7pFKB+yxZJd4tT/F4IbrUBU\n-----END PRIVATE KEY-----\n",
"client_email": "testing-private-key-encryption@tokyo-amphora-437.iam.gserviceaccount.com",
"client_id": "102915290076242385238",
"client_email": "test-credentials@my-project.iam.gserviceaccount.com",
"client_id": "12345",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/testing-private-key-encryption%40tokyo-amphora-437.iam.gserviceaccount.com"
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/testing-private-key-encryption%40my-project.iam.gserviceaccount.com"
}
@@ -31,7 +31,7 @@ defmodule Goth.ClientTest do
assert {:ok, _obj} = Poison.decode(json)
end

test "we call the API with the correct data and generate a token", %{bypass: bypass} do
test "we call the API with the correct jwt data and generate a token", %{bypass: bypass} do
token_response = %{
"access_token" => "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
"token_type" => "Bearer",
@@ -69,6 +69,45 @@ defmodule Goth.ClientTest do
assert ^generated = claims
end

test "we call the API with the correct refresh data and generate a token", %{bypass: bypass} do
# Set up a temporary config with a refresh token
normal_json = Application.get_env(:goth, :json)
refresh_json = "test/data/home/gcloud/application_default_credentials.json" |> Path.expand |> File.read!
Application.put_env(:goth, :json, refresh_json, persistent: true)
Application.stop(:goth)
Application.start(:goth)

token_response = %{
"access_token" => "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
"token_type" => "Bearer",
"expires_in" => 3600
}

scope = "prediction"

Bypass.expect bypass, fn conn ->
assert "/oauth2/v4/token" == conn.request_path
assert "POST" == conn.method

{:ok, body, _conn} = Plug.Conn.read_body(conn)
assert body =~ ~r/refresh_token=refreshrefreshrefresh/

Plug.Conn.resp(conn, 201, Poison.encode!(token_response))
end

{:ok, data} = Client.get_access_token(scope)

at = token_response["access_token"]
tt = token_response["token_type"]

assert %Token{token: ^at, type: ^tt, expires: _exp} = data

# Restore original config
Application.put_env(:goth, :json, normal_json, persistent: true)
Application.stop(:goth)
Application.start(:goth)
end

test "We call the metadata service correctly and decode the token", %{bypass: bypass} do
token_response = %{
"access_token" => "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
@@ -25,14 +25,14 @@ defmodule Goth.ConfigTest do
end

test "the initial state is what's passed in from the app config" do
state = "config/test-credentials.json" |> Path.expand |> File.read! |> Poison.decode!
state = "test/data/test-credentials.json" |> Path.expand |> File.read! |> Poison.decode!
state |> Map.keys |> Enum.each(fn(key) ->
assert {:ok, state[key]} == Config.get(key)
end)
end

test "the initial state has the token_source set to oauth" do
assert {:ok, :oauth} == Config.get(:token_source)
test "the initial state has the token_source set to oauth_jwt" do
assert {:ok, :oauth_jwt} == Config.get(:token_source)
end

test "Goth correctly retrieves project IDs from metadata", %{bypass: bypass} do
@@ -68,15 +68,15 @@ defmodule Goth.ConfigTest do
current_json = Application.get_env(:goth, :json)
Application.put_env(:goth, :json, nil, persistent: true)
System.put_env("GOOGLE_APPLICATION_CREDENTIALS",
"config/test-credentials.json")
"test/data/test-credentials-2.json")
Application.stop(:goth)

Application.start(:goth)
state = "config/test-credentials.json" |> Path.expand |> File.read! |> Poison.decode!
state = "test/data/test-credentials-2.json" |> Path.expand |> File.read! |> Poison.decode!
state |> Map.keys |> Enum.each(fn(key) ->
assert {:ok, state[key]} == Config.get(key)
end)
assert {:ok, :oauth} == Config.get(:token_source)
assert {:ok, :oauth_jwt} == Config.get(:token_source)

# Restore original config
Application.put_env(:goth, :json, current_json, persistent: true)
@@ -85,6 +85,37 @@ defmodule Goth.ConfigTest do
Application.start(:goth)
end

test "gcloud default credentials are found", %{bypass: bypass} do
# The test configuration sets an example JSON blob. We override it briefly
# during this test.
current_json = Application.get_env(:goth, :json)
current_home = Application.get_env(:goth, :config_root_dir)
Application.put_env(:goth, :json, nil, persistent: true)
Application.put_env(:goth, :config_root_dir, "test/data/home", persistent: true)
Application.stop(:goth)

# Fake project response because the ADC doesn't embed a project.
project = "test-project"
Bypass.expect(bypass, fn(conn) ->
uri = "/computeMetadata/v1/project/project-id"
assert(conn.request_path == uri, "Goth should ask for project ID")
Plug.Conn.resp(conn, 200, project)
end)

Application.start(:goth)
state = "test/data/home/gcloud/application_default_credentials.json" |> Path.expand |> File.read! |> Poison.decode!
state |> Map.keys |> Enum.each(fn(key) ->
assert {:ok, state[key]} == Config.get(key)
end)
assert {:ok, :oauth_refresh} == Config.get(:token_source)

# Restore original config
Application.put_env(:goth, :json, current_json, persistent: true)
Application.put_env(:goth, :config_root_dir, current_home, persistent: true)
Application.stop(:goth)
Application.start(:goth)
end

test "project_id can be overridden in config" do
project = "different"
Application.put_env(:goth, :project_id, project, persistent: true)

0 comments on commit 4a8b46d

Please sign in to comment.
You can’t perform that action at this time.