peburrows / goth

Elixir package for Oauth authentication via Google Cloud APIs
http://hexdocs.pm/goth
MIT License
284 stars 108 forks source link

Access token for Cloud Function #101

Closed paveltyk closed 3 years ago

paveltyk commented 3 years ago

Is there a way to get an access token for a Google Cloud Function invocation via Goth? This is the code example, but I would prefer to rely on Goth instead:

cloud_function_url = "https://<REGION>-<PROJECT_ID>.cloudfunctions.net/<FUNCTION_NAME>"

{:ok, client_email} = Goth.Config.get(:client_email)
{:ok, private_key} = Goth.Config.get(:private_key)
signer = Joken.Signer.create("RS256", %{"pem" => private_key})

claims = %{
  target_audience: cloud_function_url,
  aud: "https://www.googleapis.com/oauth2/v4/token",
  iss: client_email,
  sub: client_email,
  iat: :os.system_time(:seconds),
  exp: :os.system_time(:seconds) + 10
}

{:ok, jwt} = Joken.Signer.sign(claims, signer)

google_api_url = "https://www.googleapis.com/oauth2/v4/token"

body =
  {:form,
   [
     grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
     assertion: jwt
   ]}

headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
result = HTTPoison.post(google_api_url, body, headers)

{:ok, %{status_code: 200, body: "" <> _ = body}} = result
{:ok, %{"id_token" => google_token}} = Jason.decode(body)

Thank you!

wojtekmach commented 3 years ago

This is currently not possible, we're missing ability to pass target_audience parameter when generating the JWT token. See #58. If you'd like to explore this further and send a PR, it will be very appreciated but we don't have plans to add it ourselves at the moment. Thanks!

paveltyk commented 3 years ago

Thank you for a quick reply. If I go for a PR, should I start with v1.3.0 ?

wojtekmach commented 3 years ago

yeah, please open against the master branch which will become 1.3.0.

paveltyk commented 3 years ago

@wojtekmach I've looked at the codebase and my suggestion would be to introduce the claims override/extends to the source config, which would require replacing sub option to be replaced with claims: %{"sub" => "bob@example.com"}, i.e. Goth.start_link(name: MyApp.Goth, source: {:service_account, credentials, [sub: "bob@example.com"]}) will become Goth.start_link(name: MyApp.Goth, source: {:service_account, credentials, [claims: %{"sub" => "bob@example.com"}]}).

Then they will be merged in JWT.

Let me know if you foresee any issues with that approach. Otherwise I will build a PR. Thank you.

Regards, Pavel.

wojtekmach commented 3 years ago

sounds good! Btw, I just pushed an update to docs so make sure you pull before you work on a patch.

paveltyk commented 3 years ago

@wojtekmach Any estimates on merging https://github.com/peburrows/goth/pull/102 ?

wojtekmach commented 3 years ago

thanks for the PR, i'll try to get to it by the end of this week if not, definitely by the end of the following.

paveltyk commented 3 years ago

To sum up: As I figured out, even though Google docs ask for a target_audience param to generate cloud function invocation token, it works fine if I submit cloud function URL as a scope instead. Therefore it's possible to get a token ID for cloud function with v1.3.0 and PR https://github.com/peburrows/goth/pull/102. The config looks like this: %{source: {:service_account, credentials, scopes: [cloud_function_url]}}

paveltyk commented 3 years ago

@wojtekmach PR #102 and a little trick around scope vs target_audience allow to get cloud function invocation token. However there still room for improvement. Should I go for my previous proposal to add claims in a source config ({:service_account, credentials, [sub: "bob@example.com"]} => {:service_account, credentials, [claims: %{"sub" => "bob@example.com"}]}) ? That should address all possible edge cases if one needed.

wojtekmach commented 3 years ago

Yeah a more general solution is definitely welcome. Please remember to revert parts of the previous PR if they no longer make sense.

krishandley commented 2 years ago

Sorry to bother you, just writing this in case you happen to know. This did work initially when I had one function.

Maybe I'm doing something wrong, but doesn't seem to work if you have two functions. Adding multiple function urls to the scopes breaks all of them.

I can add Goth multiple times with different ID's to my supervision tree, I don't know if that's a terrible idea or not, but it works.

paveltyk commented 2 years ago

@krishandley since #102 has been merged in master maybe try the master branch with the claims approach? I do not remember it on top of my head, but try something like:

Goth.start_link(
  name: MyApp.Goth,
  source: {
    :service_account,
    credentials,
    [
      claims: %{
        "sub" => "bob@example.com",
        "target_audience" => "URL HERE"
      }
    ]
  }
)
krishandley commented 2 years ago

@paveltyk Thanks for the quick reply. I missed the updated docs for Goth.Token on master.

I wasn't able to figure out the config for multiple functions inside start_link. But calling this before calling a function worked, without needing the claims in the config.

Goth.Token.fetch(%{
  source: {:service_account, credentials, [
    claims: %{"target_audience" => function_url}
  ]}
})
paveltyk commented 2 years ago

@krishandley Yes, that sounds right. Those claims will be passed to individual GenServers managed by Goth.