Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC auth token for Cloud Run services requiring authentication #61

Merged
merged 23 commits into from
Mar 15, 2023

Conversation

emerson-argueta
Copy link
Contributor

As mention in issue #28, currently google cloud run services that require authentication cannot currently use cloudtasker.
These 3 commits included in the pull request aim to add the oidc auth token feature to cloudtasker.
In these commits the following changes are added

  • Add the faraday http client to the cloudtakser.gemspec
  • Add a oidc_enable flag to the config
  • Add logic to fetch the oidc auth token from the google metadata server using the faraday http client in the authenticator if oidc_enabled is set to true

Emerson Argueta added 8 commits April 20, 2022 13:23
Adding faraday dependency to use as an http client
Adding oidc_enabled flag to add feature for authenticated google cloud run services
Adding oidc_token feature for google cloud run services that require authentication
Moved method into class
moved method to class
To meet rubocop specs
Added missing error constant, fixed layout and styling to meet rubocop spec
Fix trailing whitespace to meet rubocop spec
Copy link
Member

@alachaum alachaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @emerson-argueta thanks for tackling this issue, I'm sure a lot of people will find it useful 🎉

Just a couple of questions:

  • Did you test this change end to end (using Cloud Run for example)? I will test it as well on my side to double check any edge case
  • I'm surprised you have to fetch the oidc token manually. Are you sure the GCP ruby SDK doesn't provide a prebuilt function for that?

@@ -51,5 +58,19 @@ def verify(bearer_token)
def verify!(bearer_token)
verify(bearer_token) || raise(AuthenticationError)
end

def oidc_token
google_metadata_server_url = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of manually retrieving an OIDC token from the metadata server, isn't there a prebuilt function in the GCP Ruby SDK to do so? (I haven't looked it up yet)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I was able to research, I did not find a function in the Ruby SDK to fetch the token. Here are some relevant links that I used to try to find the answer:

In the first link, you can see that certain languages have a function to fetch the token and Ruby is not included.
The second links shows all the Ruby GCP libraries. I did not find anything relevent there unless by mistake I did not see it.

Because I couldn't find a library I decided to use manually retrieve the token as described at the end of the page in the first link.

#
# @return [Boolean] Flag to enable oidc.
#
def oidc_enabled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oidc_enabled || false doesn't add much value. You can just add oidc_enabled as an accessor - similar to store_payloads_in_redis.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, will make sure to do this change,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer the first question: I manually tested using one of my rails services ( which can only be called through authenticated requests ) on google cloud run by:

  • Adding the library to my rails project fetching it from my forked repository
  • Setting the oidc_enable to true in the cloudtasker initializer of my rails project
  • Calling an endpoint on the rails service through Postman that triggers a cloudtasker worker

Although I was able to successfully run the worker from my rails service, I did not create an integration test. Again, thanks for the feedback and I really appreciate the work done on this library.

Emerson Argueta added 2 commits April 26, 2022 05:54
Refactor oidc_enabled as an accessor
Update description of oidc token fetch error
@alachaum
Copy link
Member

alachaum commented May 10, 2022

Alright so I've been digging a bit more into OIDC authentication on GCP and I don't believe this approach will work due to how OIDC tokens expire.
TL;DR; Try to schedule a job to run in two hours. If your Cloud Run service is using private invocation then the job should fail because your OIDC token has expired (they are supposed to last an hour)

The concept of OIDC auth with Cloud Run + Cloud Tasks is that:

  1. You configure your Cloud Run service to only be invoked by certain roles (it's not public anymore)
  2. Then you configure tasks to use a certain role with a certain audience. More info here

Then what happens is:

  1. When a task is due to run, Cloud Tasks will generate an idToken/AccessToken and place it as Bearer token on the Authorization header. This token is generated right before the task is sent to the handler so there is no risk of expiration.
  2. Cloud Run will verify that the role attached to the token is allowed to invoke this service and pass the task
  3. Cloudtasker receives the task and skips authentication because authorization has already been validated by Cloud Run.

So in order to support OIDC authentication, we need to do the following:

  1. Allow users to specify an oidc_token configuration via the Cloudtasker initializer. The oidc_token should contain a service_account_email and audience (we could actually infer the audience from the processor_host if no audience is specified)
  2. If an oidc_token config is specified then it should be used in lieu of an Authorization header when creating tasks. See WorkerHandler#task_payload. Also see ruby SDK doc and official doc
  3. When receiving the tasks and if an oidc_token config is specified, Cloudtasker should skip the authorization logic (because we assume that Cloud Run has vetted the request - we should add a BIG BOLD statement about that in the README). See WorkerController#authenticate!.

Let me know if that makes sense. Happy to take your view on it 😃

PS: Back onto our previous discussion regarding the metadata server - and having digged into the docs a bit more - I think you can use Google::Auth.get_application_default instead of fetching credentials manually from the metadata server. This is my source.

@emerson-argueta
Copy link
Contributor Author

Greatly appreciate the feedback and the time spend on it.
All the points made make sense on why the proposed solution would not work.

Just to make sure I understand, in order to support OIDC authentication we need to:

  1. Explain in the comments on the initializer example how to configure the oidc_token with a users service_account_email and audience.
    With this information, we would be able to create the Google::Cloud::Tasks::V2::OidcToken
    object as part of the Google::Cloud::Tasks::V2::HttpRequest oidc_token attribute that cloudtasker uses internally.
    Going this route, tokens would not expire since the Google::Cloud::Tasks::V2::HttpRequest is now taking care of fetching the OIDC token when a task is scheduled to run.

  2. Check to see if oidc_token config is set and skip adding the authorization header in WorkerHandler#task_payload

  3. Skip authentication in WorkerController#authenticate! if oidc_token config is set

I hope I understood all this correctly. Again thanks for taking the time to investigate this. 🙂

I think I could make the changes using this approach outlined in the previous comment.

@alachaum
Copy link
Member

@emerson-argueta Sorry for the late reply. Your understanding is correct. Don't hesitate to ping me for help or intermediate reviews as you progress! 👍

@alachaum
Copy link
Member

Just as an update, I recently did some investigations with the GCP team on how to best use OIDC tokens manually in Ruby.

I've summarized my approach in this gist: https://gist.github.com/alachaum/e8052d37a5584ad3f5ee37a8cbfe1492

The Google::Auth::IDTokens.verify_oidc(id_token) can be used as an additional layer of verification inside the authenticate! method. This way if the Cloud Run endpoint is left unauthenticated by mistake we still have a layer of authentication in Cloudtasker.

Just to say I haven't forgotten about this PR 😊

@emerson-argueta
Copy link
Contributor Author

Thanks, for looking further into this. It's been a while since I last worked on this but I will certainly take a look again. 👍

@alachaum alachaum self-requested a review March 15, 2023 15:15
Copy link
Member

@alachaum alachaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Going to merge this and do the final tweaks on my side.

@alachaum alachaum merged commit 3dda476 into keypup-io:master Mar 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants