Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 562b7bc

Browse filesBrowse files
fix(cloudrun): fix 'cloudrun_service_to_service_receive' sample (GoogleCloudPlatform#13372)
* fix(cloudrun): Proof of Concept - Service to Service. Implement a Flask app which receives ID tokens, and returns an HTTP response based on the validation of the token. * fix(cloudrun): Proof of Concept - fix sample to pass Unit Tests * fix(cloudrun): fixes to pass unit tests * fix(cloudrun): delete draft * fix(cloudrun): add feedback from code-assist - Add a try-except block to handle potential ValueError. * fix(cloudrun): delete placeholder variable * fix(cloudrun): fix typo * fix(cloudrun): PoC Replace gcloud commands with a Client Library * fix(cloudrun): clean up before sharing for internal review * fix(cloudrun): delete proof of concept to replace gcloud commands with a client library * Revert "fix(cloudrun): delete proof of concept to replace gcloud commands with a client library" This reverts commit 4ddb193. * Revert "fix(cloudrun): clean up before sharing for internal review" This reverts commit a8486ba. * Revert "fix(cloudrun): PoC Replace gcloud commands with a Client Library" This reverts commit 48f54a7. * fix(cloudrun): rename 'test_invalid_token' PR Review GoogleCloudPlatform#13372 (comment) * fix(cloudrun): apply feedback from PR Review GoogleCloudPlatform#13372 (review) - Change from getting the service URL from the Metadata server to an env var supplied in the Deployment - Fix comments - Change from getting the URL from the `gcloud run services descrive` to the same Client library used in app.py to unify the URL - Rename tests * fix(cloudrun): remove duplicated code to get the Service URL * fix(cloudrun): code cleanup before review * fix(cloudrun): implement reading the Service URL from an env var GoogleCloudPlatform#13372 (comment) * fix(cloudrun): directly return the id token in the fixture * fix(cloudrun): delete unused import
1 parent 800c162 commit 562b7bc
Copy full SHA for 562b7bc

File tree

Expand file treeCollapse file tree

5 files changed

+140
-74
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+140
-74
lines changed

‎run/service-auth/app.py

Copy file name to clipboardExpand all lines: run/service-auth/app.py
+81-7Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,104 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
# [START auth_validate_and_decode_bearer_token_on_flask]
16+
# [START cloudrun_service_to_service_receive]
17+
"""Demonstrates how to receive authenticated service-to-service requests
18+
on a Cloud Run Service.
19+
"""
20+
1521
from http import HTTPStatus
1622
import os
23+
from typing import Optional
1724

1825
from flask import Flask, request
1926

20-
from receive import receive_request_and_parse_auth_header
27+
from google.auth.exceptions import GoogleAuthError
28+
from google.auth.transport import requests
29+
from google.oauth2 import id_token
2130

2231
app = Flask(__name__)
2332

2433

34+
def parse_auth_header(auth_header: str) -> Optional[str]:
35+
"""Parse the authorization header, validate and decode the Bearer token.
36+
37+
Args:
38+
auth_header: Raw HTTP header with a Bearer token.
39+
40+
Returns:
41+
A string containing the email from the token.
42+
None if the token is invalid or the email can't be retrieved.
43+
"""
44+
45+
# Split the auth type and value from the header.
46+
try:
47+
auth_type, creds = auth_header.split(" ", 1)
48+
except ValueError:
49+
print("Malformed Authorization header.")
50+
return None
51+
52+
# Get the service URL from the environment variable
53+
# set at the time of deployment.
54+
service_url = os.environ["SERVICE_URL"]
55+
56+
# Define the expected audience as the Service Base URL.
57+
audience = service_url
58+
59+
# Validate and decode the ID token in the header.
60+
if auth_type.lower() == "bearer":
61+
try:
62+
# Find more information about `verify_oauth2_token` function:
63+
# https://googleapis.dev/python/google-auth/latest/reference/google.oauth2.id_token.html#google.oauth2.id_token.verify_oauth2_token
64+
decoded_token = id_token.verify_oauth2_token(
65+
id_token=creds,
66+
request=requests.Request(),
67+
audience=audience,
68+
)
69+
70+
# More info about the structure for the decoded ID Token here:
71+
# https://cloud.google.com/docs/authentication/token-types#id
72+
73+
# Verify that the token contains the email claim.
74+
if decoded_token['email_verified']:
75+
print(f"Email verified: {decoded_token['email']}")
76+
77+
return decoded_token['email']
78+
79+
print("Invalid token. Email wasn't verified.")
80+
except GoogleAuthError as e:
81+
print(f"Invalid token: {e}")
82+
else:
83+
print(f"Unhandled header format ({auth_type}).")
84+
85+
return None
86+
87+
2588
@app.route("/")
2689
def main() -> str:
27-
"""Example route for receiving authorized requests."""
90+
"""Example route for receiving authorized requests only."""
2891
try:
29-
response = receive_request_and_parse_auth_header(request)
92+
auth_header = request.headers.get("Authorization")
93+
if auth_header:
94+
email = parse_auth_header(auth_header)
95+
96+
if email:
97+
return f"Hello, {email}.\n", HTTPStatus.OK
3098

31-
status = HTTPStatus.UNAUTHORIZED
32-
if "Hello" in response:
33-
status = HTTPStatus.OK
99+
# Indicate that the request must be authenticated
100+
# and that Bearer auth is the permitted authentication scheme.
101+
headers = {"WWW-Authenticate": "Bearer"}
34102

35-
return response, status
103+
return (
104+
"Unauthorized request. Please supply a valid bearer token.",
105+
HTTPStatus.UNAUTHORIZED,
106+
headers,
107+
)
36108
except Exception as e:
37109
return f"Error verifying ID token: {e}", HTTPStatus.UNAUTHORIZED
38110

39111

40112
if __name__ == "__main__":
41113
app.run(host="localhost", port=int(os.environ.get("PORT", 8080)), debug=True)
114+
# [END cloudrun_service_to_service_receive]
115+
# [END auth_validate_and_decode_bearer_token_on_flask]

‎run/service-auth/receive.py

Copy file name to clipboardExpand all lines: run/service-auth/receive.py
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
For example for Cloud Run or Cloud Functions.
1818
"""
1919

20+
# This sample will be migrated to app.py
21+
2022
# [START auth_validate_and_decode_bearer_token_on_flask]
2123
# [START cloudrun_service_to_service_receive]
2224
from flask import Request

‎run/service-auth/receive_test.py renamed to ‎run/service-auth/receive_auth_requests_test.py

Copy file name to clipboardExpand all lines: run/service-auth/receive_auth_requests_test.py
+54-65Lines changed: 54 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
# limitations under the License.
1414

1515
# This test deploys a secure application running on Cloud Run
16-
# to test that the authentication sample works properly.
16+
# to validate receiving authenticated requests.
1717

1818
from http import HTTPStatus
1919
import os
2020
import subprocess
21-
from urllib import error, request
2221
import uuid
2322

23+
import backoff
24+
25+
from google.auth.transport import requests as transport_requests
26+
from google.oauth2 import id_token
27+
2428
import pytest
2529

2630
import requests
@@ -29,6 +33,7 @@
2933
from requests.sessions import Session
3034

3135
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
36+
REGION = "us-central1"
3237

3338
STATUS_FORCELIST = [
3439
HTTPStatus.BAD_REQUEST,
@@ -43,30 +48,55 @@
4348

4449

4550
@pytest.fixture(scope="module")
46-
def service_name() -> str:
51+
def project_number() -> str:
52+
return (
53+
subprocess.run(
54+
[
55+
"gcloud",
56+
"projects",
57+
"describe",
58+
PROJECT_ID,
59+
"--format=value(projectNumber)",
60+
],
61+
stdout=subprocess.PIPE,
62+
check=True,
63+
)
64+
.stdout.strip()
65+
.decode()
66+
)
67+
68+
69+
@pytest.fixture(scope="module")
70+
def service_url(project_number: str) -> str:
71+
"""Deploys a Run Service and returns its Base URL."""
72+
4773
# Add a unique suffix to create distinct service names.
48-
service_name_str = f"receive-{uuid.uuid4().hex}"
74+
service_name = f"receive-python-{uuid.uuid4().hex}"
4975

50-
# Deploy the Cloud Run Service.
76+
# Construct the Deterministic URL.
77+
service_url = f"https://{service_name}-{project_number}.{REGION}.run.app"
78+
79+
# Deploy the Cloud Run Service supplying the URL as an environment variable.
5180
subprocess.run(
5281
[
5382
"gcloud",
5483
"run",
5584
"deploy",
56-
service_name_str,
85+
service_name,
5786
"--project",
5887
PROJECT_ID,
5988
"--source",
6089
".",
61-
"--region=us-central1",
90+
f"--region={REGION}",
6291
"--allow-unauthenticated",
92+
f"--set-env-vars=SERVICE_URL={service_url}",
6393
"--quiet",
6494
],
6595
# Rise a CalledProcessError exception for a non-zero exit code.
6696
check=True,
6797
)
6898

69-
yield service_name_str
99+
yield service_url
70100

71101
# Clean-up after running the test.
72102
subprocess.run(
@@ -75,65 +105,27 @@ def service_name() -> str:
75105
"run",
76106
"services",
77107
"delete",
78-
service_name_str,
108+
service_name,
79109
"--project",
80110
PROJECT_ID,
81111
"--async",
82-
"--region=us-central1",
112+
f"--region={REGION}",
83113
"--quiet",
84114
],
85115
check=True,
86116
)
87117

88118

89119
@pytest.fixture(scope="module")
90-
def endpoint_url(service_name: str) -> str:
91-
endpoint_url_str = (
92-
subprocess.run(
93-
[
94-
"gcloud",
95-
"run",
96-
"services",
97-
"describe",
98-
service_name,
99-
"--project",
100-
PROJECT_ID,
101-
"--region=us-central1",
102-
"--format=value(status.url)",
103-
],
104-
stdout=subprocess.PIPE,
105-
check=True,
106-
)
107-
.stdout.strip()
108-
.decode()
109-
)
120+
def token(service_url: str) -> str:
121+
auth_req = transport_requests.Request()
122+
target_audience = service_url
110123

111-
return endpoint_url_str
124+
return id_token.fetch_id_token(auth_req, target_audience)
112125

113126

114127
@pytest.fixture(scope="module")
115-
def token() -> str:
116-
token_str = (
117-
subprocess.run(
118-
["gcloud", "auth", "print-identity-token"],
119-
stdout=subprocess.PIPE,
120-
check=True,
121-
)
122-
.stdout.strip()
123-
.decode()
124-
)
125-
126-
return token_str
127-
128-
129-
@pytest.fixture(scope="module")
130-
def client(endpoint_url: str) -> Session:
131-
req = request.Request(endpoint_url)
132-
try:
133-
_ = request.urlopen(req)
134-
except error.HTTPError as e:
135-
assert e.code == HTTPStatus.FORBIDDEN
136-
128+
def client() -> Session:
137129
retry_strategy = Retry(
138130
total=3,
139131
status_forcelist=STATUS_FORCELIST,
@@ -148,31 +140,28 @@ def client(endpoint_url: str) -> Session:
148140
return client
149141

150142

151-
def test_authentication_on_cloud_run(
152-
client: Session, endpoint_url: str, token: str
143+
@backoff.on_exception(backoff.expo, Exception, max_time=60)
144+
def test_authenticated_request(
145+
client: Session, service_url: str, token: str,
153146
) -> None:
154147
response = client.get(
155-
endpoint_url, headers={"Authorization": f"Bearer {token}"}
148+
service_url, headers={"Authorization": f"Bearer {token}"}
156149
)
157150
response_content = response.content.decode("utf-8")
158151

159152
assert response.status_code == HTTPStatus.OK
160153
assert "Hello" in response_content
161-
assert "anonymous" not in response_content
162154

163155

164-
def test_anonymous_request_on_cloud_run(client: Session, endpoint_url: str) -> None:
165-
response = client.get(endpoint_url)
166-
response_content = response.content.decode("utf-8")
156+
def test_anonymous_request(client: Session, service_url: str) -> None:
157+
response = client.get(service_url)
167158

168-
assert response.status_code == HTTPStatus.OK
169-
assert "Hello" in response_content
170-
assert "anonymous" in response_content
159+
assert response.status_code == HTTPStatus.UNAUTHORIZED
171160

172161

173-
def test_invalid_token(client: Session, endpoint_url: str) -> None:
162+
def test_invalid_token(client: Session, service_url: str) -> None:
174163
response = client.get(
175-
endpoint_url, headers={"Authorization": "Bearer i-am-not-a-real-token"}
164+
service_url, headers={"Authorization": "Bearer i-am-not-a-real-token"}
176165
)
177166

178167
assert response.status_code == HTTPStatus.UNAUTHORIZED
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pytest==8.3.5
2+
backoff==2.2.1

‎run/service-auth/requirements.txt

Copy file name to clipboard
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
google-auth==2.38.0
1+
google-auth==2.40.1
2+
google-cloud-run==0.10.18
23
requests==2.32.3
34
Flask==3.1.1
45
gunicorn==23.0.0
5-
Werkzeug==3.1.3

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.