From b720ebe65f102a43a8eb0abe5278e09a28b80069 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 28 Mar 2019 15:46:01 -0700 Subject: [PATCH 01/11] gae std py37 cps --- appengine/standard_python37/pubsub/README.md | 77 ++++++++++++++++ appengine/standard_python37/pubsub/app.yaml | 9 ++ appengine/standard_python37/pubsub/main.py | 88 +++++++++++++++++++ .../standard_python37/pubsub/main_test.py | 70 +++++++++++++++ .../standard_python37/pubsub/requirements.txt | 2 + .../pubsub/sample_message.json | 5 ++ .../pubsub/templates/index.html | 38 ++++++++ 7 files changed, 289 insertions(+) create mode 100644 appengine/standard_python37/pubsub/README.md create mode 100644 appengine/standard_python37/pubsub/app.yaml create mode 100644 appengine/standard_python37/pubsub/main.py create mode 100644 appengine/standard_python37/pubsub/main_test.py create mode 100644 appengine/standard_python37/pubsub/requirements.txt create mode 100644 appengine/standard_python37/pubsub/sample_message.json create mode 100644 appengine/standard_python37/pubsub/templates/index.html diff --git a/appengine/standard_python37/pubsub/README.md b/appengine/standard_python37/pubsub/README.md new file mode 100644 index 00000000000..1b5e1f0c886 --- /dev/null +++ b/appengine/standard_python37/pubsub/README.md @@ -0,0 +1,77 @@ +# Python Google Cloud Pub/Sub sample for Google App Engine Standard Environment + +[![Open in Cloud Shell][shell_img]][shell_link] + +[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png +[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/standard/pubsub/README.md + +This demonstrates how to send and receive messages using [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) on [Google App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). + +2. Create a topic and subscription. + + $ gcloud pubsub topics create [your-topic-name] + $ gcloud pubsub subscriptions create [your-subscription-name] \ + --topic [your-topic-name] \ + --push-endpoint \ + https://[your-app-id].appspot.com/_ah/push-handlers/receive_messages/token=[your-token] \ + --ack-deadline 30 + +3. Update the environment variables in ``app.yaml``. + +## Running locally + +Refer to the [top-level README](../README.md) for instructions on running and deploying. + +When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: + + $ gcloud init + +Install dependencies, preferably with a virtualenv: + + $ virtualenv env + $ source env/bin/activate + $ pip install -r requirements.txt + +Then set environment variables before starting your application: + + $ export GOOGLE_CLOUD_PROJECT=[your-project-name] + $ export PUBSUB_VERIFICATION_TOKEN=[your-verification-token] + $ export PUBSUB_TOPIC=[your-topic] + $ python main.py + +### Simulating push notifications + +The application can send messages locally, but it is not able to receive push messages locally. You can, however, simulate a push message by making an HTTP request to the local push notification endpoint. There is an included ``sample_message.json``. You can use +``curl`` or [httpie](https://github.com/jkbrzt/httpie) to POST this: + + $ curl -i --data @sample_message.json ":8080/_ah/push-handlers/receive_messages?token=[your-token]" + +Or + + $ http POST ":8080/_ah/push-handlers/receive_messages?token=[your-token]" < sample_message.json + +Response: + + HTTP/1.0 200 OK + Content-Length: 2 + Content-Type: text/html; charset=utf-8 + Date: Mon, 10 Aug 2015 17:52:03 GMT + Server: Werkzeug/0.10.4 Python/2.7.10 + + OK + +After the request completes, you can refresh ``localhost:8080`` and see the message in the list of received messages. + +## Running on App Engine + +Deploy using `gcloud`: + + gcloud app deploy app.yaml + +You can now access the application at `https://your-app-id.appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. diff --git a/appengine/standard_python37/pubsub/app.yaml b/appengine/standard_python37/pubsub/app.yaml new file mode 100644 index 00000000000..343d2fee22a --- /dev/null +++ b/appengine/standard_python37/pubsub/app.yaml @@ -0,0 +1,9 @@ +runtime: python37 + +#[START env] +env_variables: + PUBSUB_TOPIC: march + # This token is used to verify that requests originate from your + # application. It can be any sufficiently random string. + PUBSUB_VERIFICATION_TOKEN: 1234abc +#[END env] diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py new file mode 100644 index 00000000000..1b1a5407058 --- /dev/null +++ b/appengine/standard_python37/pubsub/main.py @@ -0,0 +1,88 @@ +# Copyright 2018 Google, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import base64 +import json +import logging +import os + +from flask import current_app, Flask, render_template, request +from google.cloud import pubsub_v1 + + +app = Flask(__name__) + +# Configure the following environment variables via app.yaml +# This is used in the push request handler to verify that the request came from +# pubsub and originated from a trusted source. +app.config['PUBSUB_VERIFICATION_TOKEN'] = \ + os.environ['PUBSUB_VERIFICATION_TOKEN'] +app.config['PUBSUB_TOPIC'] = os.environ['PUBSUB_TOPIC'] +app.config['GCLOUD_PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] + + +# Global list to storage messages received by this instance. +MESSAGES = [] + + +# [START index] +@app.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'GET': + return render_template('index.html', messages=MESSAGES) + + data = request.form.get('payload', 'Example payload').encode('utf-8') + + publisher = pubsub_v1.PublisherClient() + topic_path = publisher.topic_path(app.config['GCLOUD_PROJECT'], + app.config['PUBSUB_TOPIC']) + publisher.publish(topic_path, data) + + return 'OK', 200 +# [END index] + + +# [START push] +@app.route('/_ah/push-handlers/receive_messages', methods=['POST']) +def receive_messages_handler(): + # Verify that the request originates from the application. + if (request.args.get('token', '') != + current_app.config['PUBSUB_VERIFICATION_TOKEN']): + return 'Invalid request', 400 + # TODO: I need add some code to verify the bearer token too. + + envelope = json.loads(request.data.decode('utf-8')) + payload = base64.b64decode(envelope['message']['data']) + MESSAGES.append(payload) + + # Returning any 2xx status indicates successful receipt of the message. + return 'OK', 200 +# [END push] + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py new file mode 100644 index 00000000000..f2afb5e4a69 --- /dev/null +++ b/appengine/standard_python37/pubsub/main_test.py @@ -0,0 +1,70 @@ +# Copyright 2018 Google, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import json +import os + +import pytest + +import main + + +@pytest.fixture +def client(): + main.app.testing = True + return main.app.test_client() + + +def test_index(client): + r = client.get('/') + assert r.status_code == 200 + + +def test_post_index(client): + r = client.post('/', data={'payload': 'Test payload'}) + assert r.status_code == 200 + + +def test_push_endpoint(client): + url = '/_ah/push-handlers/receive_messages?token=' + \ + os.environ['PUBSUB_VERIFICATION_TOKEN'] + + r = client.post( + url, + data=json.dumps({ + "message": { + "data": base64.b64encode( + u'Test message'.encode('utf-8') + ).decode('utf-8') + } + }) + ) + + assert r.status_code == 200 + + # Make sure the message is visible on the home page. + r = client.get('/') + assert r.status_code == 200 + assert 'Test message' in r.data.decode('utf-8') + + +def test_push_endpoint_errors(client): + # no token + r = client.post('/_ah/push-handlers/receive_messages') + assert r.status_code == 400 + + # invalid token + r = client.post('/_ah/push-handlers/receive_messages?token=bad') + assert r.status_code == 400 diff --git a/appengine/standard_python37/pubsub/requirements.txt b/appengine/standard_python37/pubsub/requirements.txt new file mode 100644 index 00000000000..b9337b386c1 --- /dev/null +++ b/appengine/standard_python37/pubsub/requirements.txt @@ -0,0 +1,2 @@ +Flask==1.0.2 +google-cloud-pubsub==0.40.0 diff --git a/appengine/standard_python37/pubsub/sample_message.json b/appengine/standard_python37/pubsub/sample_message.json new file mode 100644 index 00000000000..8fe62d23fb9 --- /dev/null +++ b/appengine/standard_python37/pubsub/sample_message.json @@ -0,0 +1,5 @@ +{ + "message": { + "data": "SGVsbG8sIFdvcmxkIQ==" + } +} diff --git a/appengine/standard_python37/pubsub/templates/index.html b/appengine/standard_python37/pubsub/templates/index.html new file mode 100644 index 00000000000..70ebcdf43a6 --- /dev/null +++ b/appengine/standard_python37/pubsub/templates/index.html @@ -0,0 +1,38 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Pub/Sub Python on Google App Engine Standard Environment + + +
+

Messages received by this instance:

+ +

Note: because your application is likely running multiple instances, each instance will have a different list of messages.

+
+ +
+ + +
+ + + From 2443e4354f00c90b0cfefce3479613306ca024d1 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 28 Mar 2019 16:00:37 -0700 Subject: [PATCH 02/11] Add TODO --- appengine/standard_python37/pubsub/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 1b1a5407058..39ac1b61db0 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -57,12 +57,14 @@ def index(): # [START push] @app.route('/_ah/push-handlers/receive_messages', methods=['POST']) def receive_messages_handler(): - # Verify that the request originates from the application. + # Verify that the request originates from the application. if (request.args.get('token', '') != current_app.config['PUBSUB_VERIFICATION_TOKEN']): return 'Invalid request', 400 + # TODO: I need add some code to verify the bearer token too. + envelope = json.loads(request.data.decode('utf-8')) payload = base64.b64decode(envelope['message']['data']) MESSAGES.append(payload) From b0d0de49354620d858af2581ff1fd5b3935025d6 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 4 Apr 2019 09:43:41 -0700 Subject: [PATCH 03/11] public/private keys --- .../pubsub/data/privatekey.pem | 27 +++++++ .../pubsub/data/public_cert.pem | 19 +++++ appengine/standard_python37/pubsub/main.py | 38 +++++++-- .../standard_python37/pubsub/main_test.py | 78 ++++++++++++++++--- .../standard_python37/pubsub/requirements.txt | 1 + .../pubsub/templates/index.html | 15 ++++ 6 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 appengine/standard_python37/pubsub/data/privatekey.pem create mode 100644 appengine/standard_python37/pubsub/data/public_cert.pem diff --git a/appengine/standard_python37/pubsub/data/privatekey.pem b/appengine/standard_python37/pubsub/data/privatekey.pem new file mode 100644 index 00000000000..57443540ad3 --- /dev/null +++ b/appengine/standard_python37/pubsub/data/privatekey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj +7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/ +xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs +SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18 +pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk +SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk +nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq +HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y +nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9 +IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2 +YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU +Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ +vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP +B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl +aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2 +eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI +aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk +klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ +CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu +UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg +soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28 +bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH +504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL +YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx +BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg== +-----END RSA PRIVATE KEY----- diff --git a/appengine/standard_python37/pubsub/data/public_cert.pem b/appengine/standard_python37/pubsub/data/public_cert.pem new file mode 100644 index 00000000000..7af6ca3f931 --- /dev/null +++ b/appengine/standard_python37/pubsub/data/public_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 39ac1b61db0..8527161ea4e 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -14,12 +14,15 @@ # [START app] import base64 +from flask import current_app, Flask, render_template, request import json import logging import os -from flask import current_app, Flask, render_template, request +from google.auth import jwt +from google.auth.transport import requests from google.cloud import pubsub_v1 +from google.oauth2 import id_token app = Flask(__name__) @@ -32,16 +35,17 @@ app.config['PUBSUB_TOPIC'] = os.environ['PUBSUB_TOPIC'] app.config['GCLOUD_PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] - -# Global list to storage messages received by this instance. +# Global list to store messages, tokens, etc. received by this instance. MESSAGES = [] - +TOKENS = [] +HEADERS = [] +CLAIMS = [] # [START index] @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'GET': - return render_template('index.html', messages=MESSAGES) + return render_template('index.html', messages=MESSAGES, tokens=TOKENS, headers=HEADERS, claims=CLAIMS) data = request.form.get('payload', 'Example payload').encode('utf-8') @@ -57,13 +61,31 @@ def index(): # [START push] @app.route('/_ah/push-handlers/receive_messages', methods=['POST']) def receive_messages_handler(): - # Verify that the request originates from the application. + # Verify the request originates from the application. if (request.args.get('token', '') != current_app.config['PUBSUB_VERIFICATION_TOKEN']): return 'Invalid request', 400 - # TODO: I need add some code to verify the bearer token too. - + # Verify the push request originates from Cloud Pub/Sub. + try: + # Get the OpenIDConnect JWT in the "Authorization" header + # attached to the push request by Cloud Pub/Sub. + bearer_token = request.headers.get('Authorization') + token = bearer_token.split(' ')[1] + TOKENS.append(token) + + header = jwt.decode_header(token) + HEADERS.append(header) + + # Verify and decode the token. Underneath it checks the signature + # with the signed section using Google's public certs at + # https://www.googleapis.com/oauth2/v1/certs + req = requests.Request() + claim = id_token.verify_oauth2_token(token, req) + CLAIMS.append(claim) + except Exception as e: + CLAIMS.append(e) + return 'Unable to verify: ' + e, 400 envelope = json.loads(request.data.decode('utf-8')) payload = base64.b64decode(envelope['message']['data']) diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py index f2afb5e4a69..7b0c0740779 100644 --- a/appengine/standard_python37/pubsub/main_test.py +++ b/appengine/standard_python37/pubsub/main_test.py @@ -13,20 +13,63 @@ # limitations under the License. import base64 +import calendar +import datetime import json import os +from google.auth import crypt +from google.auth import jwt +from google.auth.transport import requests +from google.oauth2 import id_token + +import mock import pytest import main +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') + +with open(os.path.join(DATA_DIR, 'privatekey.pem'), 'rb') as fh: + PRIVATE_KEY_BYTES = fh.read() + +with open(os.path.join(DATA_DIR, 'public_cert.pem'), 'rb') as fh: + PUBLIC_CERT_BYTES = fh.read() + + @pytest.fixture def client(): main.app.testing = True return main.app.test_client() +@pytest.fixture +def signer(): + return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, '1') + + +@pytest.fixture +def token_factory(signer): + now = calendar.timegm(datetime.datetime.utcnow().utctimetuple()) + payload = { + 'aud': 'https://example.com/_ah/push-handlers/receive_messages?token=1234abc', + 'azp': '1234567890', + 'email': 'pubsub@example.iam.gserviceaccount.com', + 'email_verified': True, + 'exp': now, + 'iat': now + 300, + 'iss': 'https://accounts.google.com', + 'sub': '1234567890' + } + header = { + 'alg': 'RS256', + 'kid': signer.key_id, + 'typ': 'JWT' + } + return jwt.encode(signer, payload, header=header) + + def test_index(client): r = client.get('/') assert r.status_code == 200 @@ -37,20 +80,35 @@ def test_post_index(client): assert r.status_code == 200 +def _verify_oauth2_token_patch(): + real_verify = jwt.decode + + def mock_verify(): + real_verify(factory, certs=PUBLIC_CERT_BYTES, verify=True) + + return mock.patch('id_token.verify_oauth2_token', new=mock_verify) + + def test_push_endpoint(client): url = '/_ah/push-handlers/receive_messages?token=' + \ os.environ['PUBSUB_VERIFICATION_TOKEN'] - r = client.post( - url, - data=json.dumps({ - "message": { - "data": base64.b64encode( - u'Test message'.encode('utf-8') - ).decode('utf-8') - } - }) - ) + r = None + with _verify_oauth2_token_patch(): + r = client.post( + url, + data=json.dumps({ + "message": { + "data": base64.b64encode( + u'Test message'.encode('utf-8') + ).decode('utf-8') + } + }), + header=json.dumps({ + "Content-Type": "application/json", + "Authorization": "Bearer " + token_factory() + }) + ) assert r.status_code == 200 diff --git a/appengine/standard_python37/pubsub/requirements.txt b/appengine/standard_python37/pubsub/requirements.txt index b9337b386c1..e65a67eaee9 100644 --- a/appengine/standard_python37/pubsub/requirements.txt +++ b/appengine/standard_python37/pubsub/requirements.txt @@ -1,2 +1,3 @@ Flask==1.0.2 +google-auth==1.6.3 google-cloud-pubsub==0.40.0 diff --git a/appengine/standard_python37/pubsub/templates/index.html b/appengine/standard_python37/pubsub/templates/index.html index 70ebcdf43a6..de72bc1e917 100644 --- a/appengine/standard_python37/pubsub/templates/index.html +++ b/appengine/standard_python37/pubsub/templates/index.html @@ -20,6 +20,21 @@
+

Print BEARER TOKENS: + {% for token in tokens: %} +

  • {{token}}
  • + {% endfor %} +

    +

    Print HEADERS: + {% for header in headers: %} +

  • {{header}}
  • + {% endfor %} +

    +

    Print CLAIMS: + {% for claim in claims: %} +

  • {{claim}}
  • + {% endfor %} +

    Messages received by this instance:

      {% for message in messages: %} From fd3beea25d35c2efa0f396b33c7707ad3ed69037 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 4 Apr 2019 13:01:21 -0700 Subject: [PATCH 04/11] wait for publish --- appengine/standard_python37/pubsub/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 8527161ea4e..e45d82ddc68 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -40,20 +40,21 @@ TOKENS = [] HEADERS = [] CLAIMS = [] +REQUEST_HEADERS = [] # [START index] @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'GET': - return render_template('index.html', messages=MESSAGES, tokens=TOKENS, headers=HEADERS, claims=CLAIMS) + return render_template('index.html', messages=MESSAGES, tokens=TOKENS, headers=HEADERS, claims=CLAIMS, request_headers=REQUEST_HEADERS) data = request.form.get('payload', 'Example payload').encode('utf-8') publisher = pubsub_v1.PublisherClient() topic_path = publisher.topic_path(app.config['GCLOUD_PROJECT'], app.config['PUBSUB_TOPIC']) - publisher.publish(topic_path, data) - + future = publisher.publish(topic_path, data) + future.result() return 'OK', 200 # [END index] @@ -66,6 +67,8 @@ def receive_messages_handler(): current_app.config['PUBSUB_VERIFICATION_TOKEN']): return 'Invalid request', 400 + REQUEST_HEADERS.append(request.headers) + # Verify the push request originates from Cloud Pub/Sub. try: # Get the OpenIDConnect JWT in the "Authorization" header From 5ac047b37e80d9c31aeff1c0fa14d801e35776de Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Fri, 5 Apr 2019 14:47:57 -0700 Subject: [PATCH 05/11] lint/py36 tests pass --- appengine/standard_python37/pubsub/main.py | 22 +++---- .../standard_python37/pubsub/main_test.py | 65 +++++++++---------- 2 files changed, 38 insertions(+), 49 deletions(-) diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index e45d82ddc68..fe77331b27c 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -1,4 +1,4 @@ -# Copyright 2018 Google, LLC. +# Copyright 2019 Google, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -40,13 +40,14 @@ TOKENS = [] HEADERS = [] CLAIMS = [] -REQUEST_HEADERS = [] +R = [] # [START index] @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'GET': - return render_template('index.html', messages=MESSAGES, tokens=TOKENS, headers=HEADERS, claims=CLAIMS, request_headers=REQUEST_HEADERS) + return render_template('index.html', messages=MESSAGES, tokens=TOKENS, + headers=HEADERS, claims=CLAIMS) data = request.form.get('payload', 'Example payload').encode('utf-8') @@ -62,17 +63,14 @@ def index(): # [START push] @app.route('/_ah/push-handlers/receive_messages', methods=['POST']) def receive_messages_handler(): - # Verify the request originates from the application. + # Verify that the request originates from the application. if (request.args.get('token', '') != current_app.config['PUBSUB_VERIFICATION_TOKEN']): return 'Invalid request', 400 - REQUEST_HEADERS.append(request.headers) - - # Verify the push request originates from Cloud Pub/Sub. + # Verify that the push request originates from Cloud Pub/Sub. try: - # Get the OpenIDConnect JWT in the "Authorization" header - # attached to the push request by Cloud Pub/Sub. + # Get the Cloud Pub/Sub-generated JWT in the "Authorization" header. bearer_token = request.headers.get('Authorization') token = bearer_token.split(' ')[1] TOKENS.append(token) @@ -80,20 +78,18 @@ def receive_messages_handler(): header = jwt.decode_header(token) HEADERS.append(header) - # Verify and decode the token. Underneath it checks the signature + # Verify and decode the JWT. Underneath it checks the signature # with the signed section using Google's public certs at # https://www.googleapis.com/oauth2/v1/certs req = requests.Request() claim = id_token.verify_oauth2_token(token, req) CLAIMS.append(claim) except Exception as e: - CLAIMS.append(e) - return 'Unable to verify: ' + e, 400 + return 'Unable to verify: {}'.format(e), 400 envelope = json.loads(request.data.decode('utf-8')) payload = base64.b64decode(envelope['message']['data']) MESSAGES.append(payload) - # Returning any 2xx status indicates successful receipt of the message. return 'OK', 200 # [END push] diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py index 7b0c0740779..c88b0431c91 100644 --- a/appengine/standard_python37/pubsub/main_test.py +++ b/appengine/standard_python37/pubsub/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2018 Google, LLC. +# Copyright 2019 Google, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,15 +17,12 @@ import datetime import json import os +import pytest from google.auth import crypt from google.auth import jwt -from google.auth.transport import requests from google.oauth2 import id_token -import mock -import pytest - import main @@ -50,15 +47,15 @@ def signer(): @pytest.fixture -def token_factory(signer): +def fake_token(signer): now = calendar.timegm(datetime.datetime.utcnow().utctimetuple()) payload = { - 'aud': 'https://example.com/_ah/push-handlers/receive_messages?token=1234abc', + 'aud': 'https://e.io/_ah/push-handlers/receive_messages?token=1234abc', 'azp': '1234567890', 'email': 'pubsub@example.iam.gserviceaccount.com', 'email_verified': True, - 'exp': now, - 'iat': now + 300, + 'exp': now + 3600, + 'iat': now, 'iss': 'https://accounts.google.com', 'sub': '1234567890' } @@ -67,7 +64,12 @@ def token_factory(signer): 'kid': signer.key_id, 'typ': 'JWT' } - return jwt.encode(signer, payload, header=header) + yield jwt.encode(signer, payload, header=header) + + +def _verify_mocked_oauth2_token(token, request): + claims = jwt.decode(token, certs=PUBLIC_CERT_BYTES, verify=True) + return claims def test_index(client): @@ -80,36 +82,27 @@ def test_post_index(client): assert r.status_code == 200 -def _verify_oauth2_token_patch(): - real_verify = jwt.decode +def test_push_endpoint(monkeypatch, client, fake_token): + monkeypatch.setattr(id_token, 'verify_oauth2_token', + _verify_mocked_oauth2_token) - def mock_verify(): - real_verify(factory, certs=PUBLIC_CERT_BYTES, verify=True) - - return mock.patch('id_token.verify_oauth2_token', new=mock_verify) - - -def test_push_endpoint(client): url = '/_ah/push-handlers/receive_messages?token=' + \ os.environ['PUBSUB_VERIFICATION_TOKEN'] - - r = None - with _verify_oauth2_token_patch(): - r = client.post( - url, - data=json.dumps({ - "message": { - "data": base64.b64encode( - u'Test message'.encode('utf-8') - ).decode('utf-8') - } - }), - header=json.dumps({ - "Content-Type": "application/json", - "Authorization": "Bearer " + token_factory() - }) + # "".join(chr(x) for x in fake_token) + + r = client.post( + url, + data=json.dumps({ + "message": { + "data": base64.b64encode( + u'Test message'.encode('utf-8') + ).decode('utf-8') + } + }), + headers=dict( + Authorization="Bearer " + fake_token.decode('utf-8') ) - + ) assert r.status_code == 200 # Make sure the message is visible on the home page. From 1d5080e8d7f678215a124ce152573b571926c804 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Fri, 5 Apr 2019 15:08:51 -0700 Subject: [PATCH 06/11] Ready --- appengine/standard_python37/pubsub/README.md | 9 +++++---- appengine/standard_python37/pubsub/app.yaml | 2 +- appengine/standard_python37/pubsub/main.py | 10 ++++------ appengine/standard_python37/pubsub/main_test.py | 2 +- appengine/standard_python37/pubsub/requirements.txt | 1 + 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/appengine/standard_python37/pubsub/README.md b/appengine/standard_python37/pubsub/README.md index 1b5e1f0c886..f10f75bd8d1 100644 --- a/appengine/standard_python37/pubsub/README.md +++ b/appengine/standard_python37/pubsub/README.md @@ -1,4 +1,4 @@ -# Python Google Cloud Pub/Sub sample for Google App Engine Standard Environment +# Python 3 Google Cloud Pub/Sub sample for Google App Engine Standard Environment [![Open in Cloud Shell][shell_img]][shell_link] @@ -13,14 +13,15 @@ Before you can run or deploy the sample, you will need to do the following: 1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). -2. Create a topic and subscription. +2. Create a topic and subscription. The pusn auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. $ gcloud pubsub topics create [your-topic-name] - $ gcloud pubsub subscriptions create [your-subscription-name] \ + $ gcloud beta pubsub subscriptions create [your-subscription-name] \ --topic [your-topic-name] \ --push-endpoint \ https://[your-app-id].appspot.com/_ah/push-handlers/receive_messages/token=[your-token] \ --ack-deadline 30 + --push-auth-service-account=[your-service-account-email] 3. Update the environment variables in ``app.yaml``. @@ -74,4 +75,4 @@ Deploy using `gcloud`: gcloud app deploy app.yaml -You can now access the application at `https://your-app-id.appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. +You can now access the application at `https://[your-app-id].appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. diff --git a/appengine/standard_python37/pubsub/app.yaml b/appengine/standard_python37/pubsub/app.yaml index 343d2fee22a..492a16878ec 100644 --- a/appengine/standard_python37/pubsub/app.yaml +++ b/appengine/standard_python37/pubsub/app.yaml @@ -2,7 +2,7 @@ runtime: python37 #[START env] env_variables: - PUBSUB_TOPIC: march + PUBSUB_TOPIC: your-topic # This token is used to verify that requests originate from your # application. It can be any sufficiently random string. PUBSUB_VERIFICATION_TOKEN: 1234abc diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index fe77331b27c..0e43945f2b2 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -78,14 +78,12 @@ def receive_messages_handler(): header = jwt.decode_header(token) HEADERS.append(header) - # Verify and decode the JWT. Underneath it checks the signature - # with the signed section using Google's public certs at - # https://www.googleapis.com/oauth2/v1/certs - req = requests.Request() - claim = id_token.verify_oauth2_token(token, req) + # Verify and decode the JWT. Underneath it checks the signature against + # Google's public certs at https://www.googleapis.com/oauth2/v1/certs + claim = id_token.verify_oauth2_token(token, requests.Request()) CLAIMS.append(claim) except Exception as e: - return 'Unable to verify: {}'.format(e), 400 + return 'Invalid token: {}'.format(e), 400 envelope = json.loads(request.data.decode('utf-8')) payload = base64.b64decode(envelope['message']['data']) diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py index c88b0431c91..59412330781 100644 --- a/appengine/standard_python37/pubsub/main_test.py +++ b/appengine/standard_python37/pubsub/main_test.py @@ -54,8 +54,8 @@ def fake_token(signer): 'azp': '1234567890', 'email': 'pubsub@example.iam.gserviceaccount.com', 'email_verified': True, - 'exp': now + 3600, 'iat': now, + 'exp': now + 3600, 'iss': 'https://accounts.google.com', 'sub': '1234567890' } diff --git a/appengine/standard_python37/pubsub/requirements.txt b/appengine/standard_python37/pubsub/requirements.txt index e65a67eaee9..04d95eb5fbd 100644 --- a/appengine/standard_python37/pubsub/requirements.txt +++ b/appengine/standard_python37/pubsub/requirements.txt @@ -1,3 +1,4 @@ Flask==1.0.2 +google-api-python-client==1.7.8 google-auth==1.6.3 google-cloud-pubsub==0.40.0 From 8ab38a153e8eac1205ca45f39aeab206cb13f11b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Fri, 5 Apr 2019 15:21:59 -0700 Subject: [PATCH 07/11] Take out unused var --- appengine/standard_python37/pubsub/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 0e43945f2b2..8d3cfcc93f3 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -40,7 +40,6 @@ TOKENS = [] HEADERS = [] CLAIMS = [] -R = [] # [START index] @app.route('/', methods=['GET', 'POST']) From ef91df7cc317ffc6437dc3150484924cffd508ca Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Fri, 5 Apr 2019 22:20:45 -0700 Subject: [PATCH 08/11] Charlie's suggestions --- .../standard_python37/pubsub/.gcloudignore | 19 +++++++++++++++++++ appengine/standard_python37/pubsub/README.md | 16 ++++++++-------- appengine/standard_python37/pubsub/main.py | 2 +- .../pubsub/templates/index.html | 2 +- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 appengine/standard_python37/pubsub/.gcloudignore diff --git a/appengine/standard_python37/pubsub/.gcloudignore b/appengine/standard_python37/pubsub/.gcloudignore new file mode 100644 index 00000000000..a987f1123d8 --- /dev/null +++ b/appengine/standard_python37/pubsub/.gcloudignore @@ -0,0 +1,19 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg \ No newline at end of file diff --git a/appengine/standard_python37/pubsub/README.md b/appengine/standard_python37/pubsub/README.md index f10f75bd8d1..bdc90d3a582 100644 --- a/appengine/standard_python37/pubsub/README.md +++ b/appengine/standard_python37/pubsub/README.md @@ -13,7 +13,7 @@ Before you can run or deploy the sample, you will need to do the following: 1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). -2. Create a topic and subscription. The pusn auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. +2. Create a topic and subscription. The push auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. $ gcloud pubsub topics create [your-topic-name] $ gcloud beta pubsub subscriptions create [your-subscription-name] \ @@ -51,7 +51,7 @@ Then set environment variables before starting your application: The application can send messages locally, but it is not able to receive push messages locally. You can, however, simulate a push message by making an HTTP request to the local push notification endpoint. There is an included ``sample_message.json``. You can use ``curl`` or [httpie](https://github.com/jkbrzt/httpie) to POST this: - $ curl -i --data @sample_message.json ":8080/_ah/push-handlers/receive_messages?token=[your-token]" + $ curl -i --data @sample_message.json "localhost:8080/_ah/push-handlers/receive_messages?token=[your-token]" Or @@ -59,15 +59,15 @@ Or Response: - HTTP/1.0 200 OK - Content-Length: 2 + HTTP/1.0 400 BAD REQUEST Content-Type: text/html; charset=utf-8 - Date: Mon, 10 Aug 2015 17:52:03 GMT - Server: Werkzeug/0.10.4 Python/2.7.10 + Content-Length: 58 + Server: Werkzeug/0.15.2 Python/3.7.3 + Date: Sat, 06 Apr 2019 04:56:12 GMT - OK + Invalid token: 'NoneType' object has no attribute 'split' -After the request completes, you can refresh ``localhost:8080`` and see the message in the list of received messages. +The simulated push request fails because it does not have a Cloud Pub/Sub-generated JWT in the "Authorization" header. ## Running on App Engine diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 8d3cfcc93f3..17fb9b9168c 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -82,7 +82,7 @@ def receive_messages_handler(): claim = id_token.verify_oauth2_token(token, requests.Request()) CLAIMS.append(claim) except Exception as e: - return 'Invalid token: {}'.format(e), 400 + return 'Invalid token: {}\n'.format(e), 400 envelope = json.loads(request.data.decode('utf-8')) payload = base64.b64decode(envelope['message']['data']) diff --git a/appengine/standard_python37/pubsub/templates/index.html b/appengine/standard_python37/pubsub/templates/index.html index de72bc1e917..eba418842bf 100644 --- a/appengine/standard_python37/pubsub/templates/index.html +++ b/appengine/standard_python37/pubsub/templates/index.html @@ -1,5 +1,5 @@ {# -# Copyright 2015 Google Inc. All Rights Reserved. +# Copyright 2019 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 83b44ee6bcb2f324c8a22c4ff5efa3487e6cef6b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Mon, 8 Apr 2019 11:41:07 -0700 Subject: [PATCH 09/11] Les's suggestions --- .../standard_python37/pubsub/.gcloudignore | 19 ------------------- appengine/standard_python37/pubsub/README.md | 19 ++++++++++--------- appengine/standard_python37/pubsub/main.py | 7 ++++++- .../standard_python37/pubsub/main_test.py | 3 +-- 4 files changed, 17 insertions(+), 31 deletions(-) delete mode 100644 appengine/standard_python37/pubsub/.gcloudignore diff --git a/appengine/standard_python37/pubsub/.gcloudignore b/appengine/standard_python37/pubsub/.gcloudignore deleted file mode 100644 index a987f1123d8..00000000000 --- a/appengine/standard_python37/pubsub/.gcloudignore +++ /dev/null @@ -1,19 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Python pycache: -__pycache__/ -# Ignored by the build system -/setup.cfg \ No newline at end of file diff --git a/appengine/standard_python37/pubsub/README.md b/appengine/standard_python37/pubsub/README.md index bdc90d3a582..ae8b62fb8c6 100644 --- a/appengine/standard_python37/pubsub/README.md +++ b/appengine/standard_python37/pubsub/README.md @@ -13,22 +13,21 @@ Before you can run or deploy the sample, you will need to do the following: 1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). -2. Create a topic and subscription. The push auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. +2. Create a topic and subscription. The push auth service account must have Service Account Token Creator Role assigned, which can be done in the Cloud Console [IAM & admin](https://console.cloud.google.com/iam-admin/iam) UI. `--push-auth-token-audience` is optional. If set, remember to modify the audience field check in `main.py` (line 88). $ gcloud pubsub topics create [your-topic-name] $ gcloud beta pubsub subscriptions create [your-subscription-name] \ - --topic [your-topic-name] \ - --push-endpoint \ + --topic=[your-topic-name] \ + --push-endpoint=\ https://[your-app-id].appspot.com/_ah/push-handlers/receive_messages/token=[your-token] \ - --ack-deadline 30 - --push-auth-service-account=[your-service-account-email] + --ack-deadline=30 \ + --push-auth-service-account=[your-service-account-email] \ + --push-auth-token-audience=example.com 3. Update the environment variables in ``app.yaml``. ## Running locally -Refer to the [top-level README](../README.md) for instructions on running and deploying. - When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: $ gcloud init @@ -71,8 +70,10 @@ The simulated push request fails because it does not have a Cloud Pub/Sub-genera ## Running on App Engine -Deploy using `gcloud`: +Note: Not all the files in the current directory are needed to run your code on App Engine. Specifically, `main_test.py` and the `data` directory, which contains a mocked private key file and a mocked public certs file, are for testing purposes only. You may feel free to leave them out or remove them when deploying your app. IWhen your app is deployed, Cloud Pub/Sub creates tokens using a private key, then the Google Auth Python library takes care of verifying and decoding the token using Google's public certs, to confirm that the push requests indeed come from Cloud Pub/Sub. + +In the current directory, deploy using `gcloud`: - gcloud app deploy app.yaml + $ gcloud app deploy app.yaml You can now access the application at `https://[your-app-id].appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index 17fb9b9168c..a622e40bc1d 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -78,9 +78,14 @@ def receive_messages_handler(): HEADERS.append(header) # Verify and decode the JWT. Underneath it checks the signature against - # Google's public certs at https://www.googleapis.com/oauth2/v1/certs + # Google's public certs at https://www.googleapis.com/oauth2/v1/certs. + # It also checks the token expiration time. claim = id_token.verify_oauth2_token(token, requests.Request()) CLAIMS.append(claim) + + # Check the claim audience field. It was specified in + # `--push-auth-token-audience` when setting up the subscription. + assert claim['aud'] == 'example.com' except Exception as e: return 'Invalid token: {}\n'.format(e), 400 diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py index 59412330781..1f439a499e3 100644 --- a/appengine/standard_python37/pubsub/main_test.py +++ b/appengine/standard_python37/pubsub/main_test.py @@ -50,7 +50,7 @@ def signer(): def fake_token(signer): now = calendar.timegm(datetime.datetime.utcnow().utctimetuple()) payload = { - 'aud': 'https://e.io/_ah/push-handlers/receive_messages?token=1234abc', + 'aud': 'example.com', 'azp': '1234567890', 'email': 'pubsub@example.iam.gserviceaccount.com', 'email_verified': True, @@ -88,7 +88,6 @@ def test_push_endpoint(monkeypatch, client, fake_token): url = '/_ah/push-handlers/receive_messages?token=' + \ os.environ['PUBSUB_VERIFICATION_TOKEN'] - # "".join(chr(x) for x in fake_token) r = client.post( url, From 052bc862986ee8832c4a70e05f7ea8aa7a0a58a6 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Mon, 8 Apr 2019 15:29:29 -0700 Subject: [PATCH 10/11] Call out ignore files in code --- .../standard_python37/pubsub/.gcloudignore | 19 +++++++++++++++++++ appengine/standard_python37/pubsub/README.md | 2 +- appengine/standard_python37/pubsub/main.py | 4 ++-- .../standard_python37/pubsub/main_test.py | 3 +++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 appengine/standard_python37/pubsub/.gcloudignore diff --git a/appengine/standard_python37/pubsub/.gcloudignore b/appengine/standard_python37/pubsub/.gcloudignore new file mode 100644 index 00000000000..a987f1123d8 --- /dev/null +++ b/appengine/standard_python37/pubsub/.gcloudignore @@ -0,0 +1,19 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg \ No newline at end of file diff --git a/appengine/standard_python37/pubsub/README.md b/appengine/standard_python37/pubsub/README.md index ae8b62fb8c6..6cb534990e8 100644 --- a/appengine/standard_python37/pubsub/README.md +++ b/appengine/standard_python37/pubsub/README.md @@ -70,7 +70,7 @@ The simulated push request fails because it does not have a Cloud Pub/Sub-genera ## Running on App Engine -Note: Not all the files in the current directory are needed to run your code on App Engine. Specifically, `main_test.py` and the `data` directory, which contains a mocked private key file and a mocked public certs file, are for testing purposes only. You may feel free to leave them out or remove them when deploying your app. IWhen your app is deployed, Cloud Pub/Sub creates tokens using a private key, then the Google Auth Python library takes care of verifying and decoding the token using Google's public certs, to confirm that the push requests indeed come from Cloud Pub/Sub. +Note: Not all the files in the current directory are needed to run your code on App Engine. Specifically, `main_test.py` and the `data` directory, which contains a mocked private key file and a mocked public certs file, are for testing purposes only. They SHOULD NOT be included in when deploying your app. When your app is up and running, Cloud Pub/Sub creates tokens using a private key, then the Google Auth Python library takes care of verifying and decoding the token using Google's public certs, to confirm that the push requests indeed come from Cloud Pub/Sub. In the current directory, deploy using `gcloud`: diff --git a/appengine/standard_python37/pubsub/main.py b/appengine/standard_python37/pubsub/main.py index a622e40bc1d..d144b940e0a 100644 --- a/appengine/standard_python37/pubsub/main.py +++ b/appengine/standard_python37/pubsub/main.py @@ -83,8 +83,8 @@ def receive_messages_handler(): claim = id_token.verify_oauth2_token(token, requests.Request()) CLAIMS.append(claim) - # Check the claim audience field. It was specified in - # `--push-auth-token-audience` when setting up the subscription. + # Check the audience field in the claim. It was specified in + # `--push-auth-token-audience` when you created the subscription. assert claim['aud'] == 'example.com' except Exception as e: return 'Invalid token: {}\n'.format(e), 400 diff --git a/appengine/standard_python37/pubsub/main_test.py b/appengine/standard_python37/pubsub/main_test.py index 1f439a499e3..fdc38faa384 100644 --- a/appengine/standard_python37/pubsub/main_test.py +++ b/appengine/standard_python37/pubsub/main_test.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# This file is for testing purposes only. You SHOULD NOT include it +# or the PEM files when deploying your app. + import base64 import calendar import datetime From e25c28501fec2f6b701ab9feaeb542bfe9acd5d4 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Mon, 8 Apr 2019 15:33:56 -0700 Subject: [PATCH 11/11] remove gcloudignore --- .../standard_python37/pubsub/.gcloudignore | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 appengine/standard_python37/pubsub/.gcloudignore diff --git a/appengine/standard_python37/pubsub/.gcloudignore b/appengine/standard_python37/pubsub/.gcloudignore deleted file mode 100644 index a987f1123d8..00000000000 --- a/appengine/standard_python37/pubsub/.gcloudignore +++ /dev/null @@ -1,19 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Python pycache: -__pycache__/ -# Ignored by the build system -/setup.cfg \ No newline at end of file