-
Notifications
You must be signed in to change notification settings - Fork 6.5k
fix(cloudrun): fix 'cloudrun_service_to_service_receive' sample #13372
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
Changes from 6 commits
c7c5225
13e85ff
84d28ed
3b22aaa
e545d67
adaf034
48023d8
48f54a7
a8486ba
4ddb193
a494fa8
29279b3
5908c4b
211862d
3a2fc62
84cfde0
c720910
0884478
1b69539
06488fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,30 +12,133 @@ | |
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
# [START auth_validate_and_decode_bearer_token_on_flask] | ||
# [START cloudrun_service_to_service_receive] | ||
"""Demonstrates how to receive authenticated service-to-service requests | ||
on a Cloud Run Service. | ||
""" | ||
|
||
from http import HTTPStatus | ||
import os | ||
from typing import Optional | ||
from urllib.request import Request, urlopen | ||
|
||
from flask import Flask, request | ||
|
||
from receive import receive_request_and_parse_auth_header | ||
from google.auth.exceptions import GoogleAuthError | ||
from google.auth.transport import requests | ||
from google.cloud import run_v2 | ||
from google.oauth2 import id_token | ||
|
||
# Get the Service Name as found in Cloud Run. | ||
SERVICE_NAME = os.getenv("K_SERVICE") | ||
|
||
# Get the Project ID. | ||
req = Request("http://metadata.google.internal/computeMetadata/v1/project/project-id") | ||
req.add_header("Metadata-Flavor", "Google") | ||
PROJECT_ID = urlopen(req).read().decode("utf-8") | ||
|
||
# Get the Region. | ||
req = Request("http://metadata.google.internal/computeMetadata/v1/instance/region") | ||
req.add_header("Metadata-Flavor", "Google") | ||
|
||
# Returns "projects/PROJECT-NUMBER/regions/REGION" | ||
project_region_list = urlopen(req).read().decode("utf-8").split('/') | ||
|
||
REGION = project_region_list[3] | ||
|
||
# Get the Service URL, required to define the valid audience for the Token. | ||
# https://cloud.google.com/run/docs/triggering/https-request#deterministic | ||
FULL_SERVICE_NAME = f"projects/{PROJECT_ID}/locations/{REGION}/services/{SERVICE_NAME}" | ||
|
||
client = run_v2.ServicesClient() | ||
|
||
service_request = run_v2.GetServiceRequest( | ||
name=FULL_SERVICE_NAME, | ||
) | ||
|
||
SERVICE_URI = client.get_service(request=service_request).uri | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can tell from previous experience that this is a lot of code to get the current service's URL. I would suggest that you encapsulate this in a method to describe what's going on here. From memory calling the metadata server here requires permissions on the service in order to read this data (roles/run.viewer, from memory) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have changed getting the
eapl-gemugami marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
app = Flask(__name__) | ||
|
||
|
||
def parse_auth_header(auth_header: str) -> Optional[str]: | ||
"""Parse the authorization header, validate and decode the Bearer token. | ||
|
||
Args: | ||
auth_header: Raw HTTP header with a Bearer token. | ||
|
||
Returns: | ||
A string containing the email from the token. | ||
None if the token is invalid or the email can't be retrieved. | ||
""" | ||
|
||
# Split the auth type and value from the header. | ||
try: | ||
auth_type, creds = auth_header.split(" ", 1) | ||
except ValueError: | ||
print("Malformed Authorization header.") | ||
return None | ||
|
||
# The token audience will be the SERVICE_URL. | ||
# If `audience` was None, it won't be verified. | ||
audience = SERVICE_URI | ||
eapl-gemugami marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if auth_type.lower() == "bearer": | ||
# Get the ID token. | ||
# Find more info about the ID Token here: | ||
# https://cloud.google.com/docs/authentication/token-types#id | ||
|
||
try: | ||
# Find more information about `verify_oauth2_token` function here: | ||
# https://googleapis.dev/python/google-auth/latest/reference/google.oauth2.id_token.html#google.oauth2.id_token.verify_oauth2_token | ||
glasnt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
decoded_token = id_token.verify_oauth2_token( | ||
id_token=creds, | ||
request=requests.Request(), | ||
audience=audience, | ||
) | ||
|
||
# Verify that the token contains the email claims. | ||
eapl-gemugami marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if decoded_token['email_verified']: | ||
print(f"Email verified {decoded_token['email']}") | ||
|
||
return decoded_token['email'] | ||
|
||
print("Email wasn't verified.") | ||
return None | ||
except GoogleAuthError as e: | ||
print(f"Invalid token: {e}") | ||
else: | ||
print(f"Unhandled header format ({auth_type}).") | ||
|
||
return None | ||
|
||
|
||
@app.route("/") | ||
def main() -> str: | ||
"""Example route for receiving authorized requests.""" | ||
"""Example route for receiving authorized requests only.""" | ||
try: | ||
response = receive_request_and_parse_auth_header(request) | ||
auth_header = request.headers.get("Authorization") | ||
if auth_header: | ||
email = parse_auth_header(auth_header) | ||
|
||
if email: | ||
return f"Hello, {email}.\n", HTTPStatus.OK | ||
|
||
status = HTTPStatus.UNAUTHORIZED | ||
if "Hello" in response: | ||
status = HTTPStatus.OK | ||
# Indicate that the request must be authenticated | ||
# and that Bearer auth is the permitted authentication scheme. | ||
headers = {"WWW-Authenticate": "Bearer"} | ||
|
||
return response, status | ||
return ( | ||
"Unauthorized request. Please supply a valid bearer token.", | ||
HTTPStatus.UNAUTHORIZED, | ||
headers, | ||
) | ||
except Exception as e: | ||
return f"Error verifying ID token: {e}", HTTPStatus.UNAUTHORIZED | ||
eapl-gemugami marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
if __name__ == "__main__": | ||
app.run(host="localhost", port=int(os.environ.get("PORT", 8080)), debug=True) | ||
# [END cloudrun_service_to_service_receive] | ||
# [END auth_validate_and_decode_bearer_token_on_flask] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
pytest==8.3.5 | ||
backoff==2.2.1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
google-auth==2.38.0 | ||
google-auth==2.40.1 | ||
google-cloud-run==0.10.18 | ||
requests==2.32.3 | ||
Flask==3.1.0 | ||
gunicorn==23.0.0 | ||
Werkzeug==3.1.3 |
Uh oh!
There was an error while loading. Please reload this page.